chore: first setup ok

refactor/issue-1/first-setup
Frédérik Benoist 2023-12-10 12:40:54 +01:00
parent 4840b31c96
commit af7e775216
24 changed files with 709 additions and 147 deletions

2
auto-imports.d.ts vendored
View File

@ -509,6 +509,7 @@ declare module 'vue' {
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']> readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']> readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly urlValidator: UnwrapRef<typeof import('./src/@core/utils/validators')['urlValidator']> readonly urlValidator: UnwrapRef<typeof import('./src/@core/utils/validators')['urlValidator']>
readonly useAbility: UnwrapRef<typeof import('./src/plugins/casl/composables/useAbility')['useAbility']>
readonly useAbs: UnwrapRef<typeof import('@vueuse/math')['useAbs']> readonly useAbs: UnwrapRef<typeof import('@vueuse/math')['useAbs']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']> readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']> readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
@ -852,6 +853,7 @@ declare module '@vue/runtime-core' {
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']> readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']> readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly urlValidator: UnwrapRef<typeof import('./src/@core/utils/validators')['urlValidator']> readonly urlValidator: UnwrapRef<typeof import('./src/@core/utils/validators')['urlValidator']>
readonly useAbility: UnwrapRef<typeof import('./src/plugins/casl/composables/useAbility')['useAbility']>
readonly useAbs: UnwrapRef<typeof import('@vueuse/math')['useAbs']> readonly useAbs: UnwrapRef<typeof import('@vueuse/math')['useAbs']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']> readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']> readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>

View File

@ -5,7 +5,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vuexy - Vuejs Admin Dashboard Template</title> <title>HelpDesk Web POS</title>
<link rel="stylesheet" type="text/css" href="/loader.css" /> <link rel="stylesheet" type="text/css" href="/loader.css" />
</head> </head>

View File

@ -12,26 +12,11 @@
class="mx-1" class="mx-1"
/> />
By <a By <a
href="https://pixinvent.com" href="https://inetum.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="text-primary ms-1" class="text-primary ms-1"
>Pixinvent</a> >Inetum</a>
</span>
<!-- 👉 Footer: right content -->
<span class="d-md-flex gap-x-4 text-primary d-none">
<a
href="https://themeforest.net/licenses/standard"
target="noopener noreferrer"
>License</a>
<a
href="https://1.envato.market/pixinvent_portfolio"
target="noopener noreferrer"
>More Themes</a>
<a
href="https://demos.pixinvent.com/vuexy-vuejs-admin-template/documentation/"
target="noopener noreferrer"
>Documentation</a>
</span> </span>
</div> </div>
</template> </template>

View File

@ -1,5 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import avatar1 from '@images/avatars/avatar-1.png' import avatar1 from '@images/avatars/avatar-1.png'
import { userStore } from '@stores/user.store'
const useUserStore = userStore()
const router = useRouter()
const logoutAndRedirect = () => {
useUserStore.logout()
router.push('/login')
}
</script> </script>
<template> <template>
@ -48,9 +58,9 @@ import avatar1 from '@images/avatars/avatar-1.png'
</template> </template>
<VListItemTitle class="font-weight-semibold"> <VListItemTitle class="font-weight-semibold">
John Doe {{ useUserStore.username }}
</VListItemTitle> </VListItemTitle>
<VListItemSubtitle>Admin</VListItemSubtitle> <VListItemSubtitle>{{ useUserStore.role }}</VListItemSubtitle>
</VListItem> </VListItem>
<VDivider class="my-2" /> <VDivider class="my-2" />
@ -111,7 +121,7 @@ import avatar1 from '@images/avatars/avatar-1.png'
<VDivider class="my-2" /> <VDivider class="my-2" />
<!-- 👉 Logout --> <!-- 👉 Logout -->
<VListItem to="/login"> <VListItem @click="logoutAndRedirect">
<template #prepend> <template #prepend>
<VIcon <VIcon
class="me-2" class="me-2"

View File

@ -9,4 +9,9 @@ export default [
to: { name: 'second-page' }, to: { name: 'second-page' },
icon: { icon: 'tabler-file' }, icon: { icon: 'tabler-file' },
}, },
{
title: 'Store',
to: { name: 'store' },
icon: { icon: 'tabler-file' },
},
] ]

View File

@ -9,4 +9,9 @@ export default [
to: { name: 'second-page' }, to: { name: 'second-page' },
icon: { icon: 'tabler-file' }, icon: { icon: 'tabler-file' },
}, },
{
title: 'Store',
to: { name: 'store' },
icon: { icon: 'tabler-file' },
},
] ]

View File

@ -1,5 +1,6 @@
<!-- Errors in the form are set on line 60 -->
<script setup lang="ts"> <script setup lang="ts">
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue' import { VForm } from 'vuetify/components/VForm'
import { useGenerateImageVariant } from '@core/composable/useGenerateImageVariant' import { useGenerateImageVariant } from '@core/composable/useGenerateImageVariant'
import authV2LoginIllustrationBorderedDark from '@images/pages/auth-v2-login-illustration-bordered-dark.png' import authV2LoginIllustrationBorderedDark from '@images/pages/auth-v2-login-illustration-bordered-dark.png'
import authV2LoginIllustrationBorderedLight from '@images/pages/auth-v2-login-illustration-bordered-light.png' import authV2LoginIllustrationBorderedLight from '@images/pages/auth-v2-login-illustration-bordered-light.png'
@ -8,30 +9,62 @@ import authV2LoginIllustrationLight from '@images/pages/auth-v2-login-illustrati
import authV2MaskDark from '@images/pages/misc-mask-dark.png' import authV2MaskDark from '@images/pages/misc-mask-dark.png'
import authV2MaskLight from '@images/pages/misc-mask-light.png' import authV2MaskLight from '@images/pages/misc-mask-light.png'
import { VNodeRenderer } from '@layouts/components/VNodeRenderer' import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
import { userStore } from '@stores/user.store'
import { themeConfig } from '@themeConfig' import { themeConfig } from '@themeConfig'
const authThemeImg = useGenerateImageVariant(authV2LoginIllustrationLight, authV2LoginIllustrationDark, authV2LoginIllustrationBorderedLight, authV2LoginIllustrationBorderedDark, true)
const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
definePage({ definePage({
meta: { meta: {
layout: 'blank', layout: 'blank',
unauthenticatedOnly: true,
}, },
}) })
const form = ref({
email: '',
password: '',
remember: false,
})
const isPasswordVisible = ref(false) const isPasswordVisible = ref(false)
const authThemeImg = useGenerateImageVariant( const route = useRoute()
authV2LoginIllustrationLight, const router = useRouter()
authV2LoginIllustrationDark,
authV2LoginIllustrationBorderedLight,
authV2LoginIllustrationBorderedDark,
true)
const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark) const ability = useAbility()
const errors = ref<Record<string, string | undefined>>({
username: undefined,
password: undefined,
})
const refVForm = ref<VForm>()
const credentials = ref({
username: 'Fred',
password: 'admin123',
})
const rememberMe = ref(false)
const isSnackbarVisibility = ref(false)
const useUserStore = userStore()
const login = async () => {
// update the userStore with the user data
useUserStore.login(credentials.value.username, credentials.value.password, ability)
// Redirect to `to` query if exist or redirect to index route
// nextTick is required to wait for DOM updates and later redirect
await nextTick(() => {
router.replace(route.query.to ? String(route.query.to) : '/')
})
}
const onSubmit = () => {
refVForm.value?.validate()
.then(({ valid: isValid }) => {
if (isValid)
login()
})
}
</script> </script>
<template> <template>
@ -40,8 +73,8 @@ const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
class="auth-wrapper bg-surface" class="auth-wrapper bg-surface"
> >
<VCol <VCol
md="8" lg="8"
class="d-none d-md-flex" class="d-none d-lg-flex"
> >
<div class="position-relative bg-background rounded-lg w-100 ma-8 me-0"> <div class="position-relative bg-background rounded-lg w-100 ma-8 me-0">
<div class="d-flex align-center justify-center w-100 h-100"> <div class="d-flex align-center justify-center w-100 h-100">
@ -53,15 +86,15 @@ const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
</div> </div>
<VImg <VImg
class="auth-footer-mask"
:src="authThemeMask" :src="authThemeMask"
class="auth-footer-mask"
/> />
</div> </div>
</VCol> </VCol>
<VCol <VCol
cols="12" cols="12"
md="4" lg="4"
class="auth-card-v2 d-flex align-center justify-center" class="auth-card-v2 d-flex align-center justify-center"
> >
<VCard <VCard
@ -74,49 +107,64 @@ const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
:nodes="themeConfig.app.logo" :nodes="themeConfig.app.logo"
class="mb-6" class="mb-6"
/> />
<h4 class="text-h4 mb-1"> <h4 class="text-h4 mb-1">
Welcome to <span class="text-capitalize">{{ themeConfig.app.title }}</span>! 👋🏻 Welcome to <span class="text-capitalize"> {{ themeConfig.app.title }} </span>! 👋🏻
</h4> </h4>
<p class="mb-0"> <p class="mb-0">
Please sign-in to your account and start the adventure Please sign-in to your account and start the adventure
</p> </p>
</VCardText> </VCardText>
<VCardText> <VCardText>
<VForm @submit.prevent="() => { }"> <VAlert
color="primary"
variant="tonal"
>
<p class="text-sm mb-2">
Admin username: <strong>admin</strong> / Pass: <strong>admin</strong>
</p>
<p class="text-sm mb-0">
Support username: <strong>support</strong> / Pass: <strong>support</strong>
</p>
</VAlert>
</VCardText>
<VCardText>
<VForm
ref="refVForm"
@submit.prevent="onSubmit"
>
<VRow> <VRow>
<!-- email --> <!-- username -->
<VCol cols="12"> <VCol cols="12">
<AppTextField <AppTextField
v-model="form.email" v-model="credentials.username"
label="Username"
placeholder="johndoe"
type="username"
autofocus autofocus
label="Email" :rules="[requiredValidator]"
type="email" :error-messages="errors.username"
placeholder="johndoe@email.com"
/> />
</VCol> </VCol>
<!-- password --> <!-- password -->
<VCol cols="12"> <VCol cols="12">
<AppTextField <AppTextField
v-model="form.password" v-model="credentials.password"
label="Password" label="Password"
placeholder="············" placeholder="············"
:rules="[requiredValidator]"
:type="isPasswordVisible ? 'text' : 'password'" :type="isPasswordVisible ? 'text' : 'password'"
:error-messages="errors.password"
:append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'" :append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
@click:append-inner="isPasswordVisible = !isPasswordVisible" @click:append-inner="isPasswordVisible = !isPasswordVisible"
/> />
<div class="d-flex align-center flex-wrap justify-space-between mt-2 mb-4"> <div class="d-flex align-center flex-wrap justify-space-between mt-1 mb-4">
<VCheckbox <VCheckbox
v-model="form.remember" v-model="rememberMe"
label="Remember me" label="Remember me"
/> />
<a
class="text-primary ms-2 mb-1"
href="#"
>
Forgot Password?
</a>
</div> </div>
<VBtn <VBtn
@ -126,46 +174,28 @@ const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
Login Login
</VBtn> </VBtn>
</VCol> </VCol>
<!-- create account -->
<VCol
cols="12"
class="text-center text-base"
>
<span>New on our platform?</span>
<a
class="text-primary ms-2"
href="#"
>
Create an account
</a>
</VCol>
<VCol
cols="12"
class="d-flex align-center"
>
<VDivider />
<span class="mx-4">or</span>
<VDivider />
</VCol>
<!-- auth providers -->
<VCol
cols="12"
class="text-center"
>
<AuthProvider />
</VCol>
</VRow> </VRow>
</VForm> </VForm>
</VCardText> </VCardText>
</VCard> </VCard>
</VCol> </VCol>
</VRow> </VRow>
<!-- Snackbar -->
<VSnackbar
v-model="isSnackbarVisibility"
location="center"
>
Incorrect username or password ...
<template #actions>
<VBtn
color="error"
@click="isSnackbarVisibility = false"
>
Close
</VBtn>
</template>
</VSnackbar>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@ -1,13 +1,418 @@
<template> <script setup lang="ts">
<div> import { VDataTableServer } from 'vuetify/labs/VDataTable'
<VCard title="Create Awesome 🙌"> import { paginationMeta } from '@api-utils/paginationMeta'
<VCardText>This is your second page.</VCardText>
<VCardText> const widgetData = ref([
Chocolate sesame snaps pie carrot cake pastry pie lollipop muffin. { title: 'In-Store Sales', value: '$5,345.43', icon: 'tabler-home', desc: '5k orders', change: 5.7 },
Carrot cake dragée chupa chups jujubes. Macaroon liquorice cookie { title: 'Website Sales', value: '$674,347.12', icon: 'tabler-device-laptop', desc: '21k orders', change: 12.4 },
wafer tart marzipan bonbon. Gingerbread jelly-o dragée { title: 'Discount', value: '$14,235.12', icon: 'tabler-gift', desc: '6k orders' },
chocolate. { title: 'Affiliate', value: '$8,345.23', icon: 'tabler-wallet', desc: '150 orders', change: -3.5 },
</VCardText> ])
</VCard>
</div> const headers = [
</template> { title: 'Product', key: 'product' },
{ title: 'Category', key: 'category' },
{ title: 'Stock', key: 'stock', sortable: false },
{ title: 'SKU', key: 'sku' },
{ title: 'Price', key: 'price' },
{ title: 'QTY', key: 'qty' },
{ title: 'Status', key: 'status' },
{ title: 'Actions', key: 'actions', sortable: false },
]
const selectedStatus = ref()
const selectedCategory = ref()
const selectedStock = ref<boolean | undefined>()
const searchQuery = ref('')
const status = ref([
{ title: 'Scheduled', value: 'Scheduled' },
{ title: 'Publish', value: 'Published' },
{ title: 'Inactive', value: 'Inactive' },
])
const categories = ref([
{ title: 'Accessories', value: 'Accessories' },
{ title: 'Home Decor', value: 'Home Decor' },
{ title: 'Electronics', value: 'Electronics' },
{ title: 'Shoes', value: 'Shoes' },
{ title: 'Office', value: 'Office' },
{ title: 'Games', value: 'Games' },
])
const stockStatus = ref([
{ title: 'In Stock', value: true },
{ title: 'Out of Stock', value: false },
])
// Data table options
const itemsPerPage = ref(10)
const page = ref(1)
const sortBy = ref()
const orderBy = ref()
// Update data table options
const updateOptions = (options: any) => {
page.value = options.page
sortBy.value = options.sortBy[0]?.key
orderBy.value = options.sortBy[0]?.order
}
const resolveCategory = (category: string) => {
if (category === 'Accessories')
return { color: 'error', icon: 'tabler-device-watch' }
if (category === 'Home Decor')
return { color: 'info', icon: 'tabler-home' }
if (category === 'Electronics')
return { color: 'primary', icon: 'tabler-device-imac' }
if (category === 'Shoes')
return { color: 'success', icon: 'tabler-shoe' }
if (category === 'Office')
return { color: 'warning', icon: 'tabler-briefcase' }
if (category === 'Games')
return { color: 'primary', icon: 'tabler-device-gamepad-2' }
}
const resolveStatus = (statusMsg: string) => {
if (statusMsg === 'Scheduled')
return { text: 'Scheduled', color: 'warning' }
if (statusMsg === 'Published')
return { text: 'Publish', color: 'success' }
if (statusMsg === 'Inactive')
return { text: 'Inactive', color: 'error' }
}
const { data: productsData, execute: fetchProducts } = await useApi<any>(createUrl('/apps/ecommerce/products',
{
query: {
q: searchQuery,
stock: selectedStock,
category: selectedCategory,
status: selectedStatus,
page,
itemsPerPage,
sortBy,
orderBy,
},
},
))
const products = computed(() => productsData.value.products)
const totalProduct = computed(() => productsData.value.total)
const deleteProduct = async (id: number) => {
await $api(`apps/ecommerce/products/${id}`, {
method: 'DELETE',
})
fetchProducts()
}
</script>
<template>
<div>
<!-- 👉 widgets -->
<VCard class="mb-6">
<VCardText>
<VRow>
<template
v-for="(data, id) in widgetData"
:key="id"
>
<VCol
cols="12"
sm="6"
md="3"
class="px-6"
>
<div
class="d-flex justify-space-between"
:class="$vuetify.display.xs
? 'product-widget'
: $vuetify.display.sm
? id < 2 ? 'product-widget' : ''
: ''"
>
<div class="d-flex flex-column gap-y-1">
<div class="text-body-1 font-weight-medium text-capitalize">
{{ data.title }}
</div>
<h4 class="text-h4 my-1">
{{ data.value }}
</h4>
<div class="d-flex">
<div class="me-2 text-disabled text-no-wrap">
{{ data.desc }}
</div>
<VChip
v-if="data.change"
label
:color="data.change > 0 ? 'success' : 'error'"
>
{{ prefixWithPlus(data.change) }}%
</VChip>
</div>
</div>
<VAvatar
variant="tonal"
rounded
size="38"
>
<VIcon
:icon="data.icon"
size="28"
/>
</VAvatar>
</div>
</VCol>
<VDivider
v-if="$vuetify.display.mdAndUp ? id !== widgetData.length - 1
: $vuetify.display.smAndUp ? id % 2 === 0
: false"
vertical
inset
length="95"
/>
</template>
</VRow>
</VCardText>
</VCard>
<!-- 👉 products -->
<VCard
title="Filters"
class="mb-6"
>
<VCardText>
<VRow>
<!-- 👉 Select Status -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedStatus"
placeholder="Status"
:items="status"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select Category -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedCategory"
placeholder="Category"
:items="categories"
clearable
clear-icon="tabler-x"
/>
</VCol>
<!-- 👉 Select Stock Status -->
<VCol
cols="12"
sm="4"
>
<AppSelect
v-model="selectedStock"
placeholder="Stock"
:items="stockStatus"
clearable
clear-icon="tabler-x"
/>
</VCol>
</VRow>
</VCardText>
<VDivider class="my-4" />
<div class="d-flex flex-wrap gap-4 mx-5">
<div class="d-flex align-center">
<!-- 👉 Search -->
<AppTextField
v-model="searchQuery"
placeholder="Search Product"
density="compact"
style="inline-size: 200px;"
class="me-3"
/>
</div>
<VSpacer />
<div class="d-flex gap-4 flex-wrap align-center">
<AppSelect
v-model="itemsPerPage"
:items="[5, 10, 20, 25, 50]"
/>
<!-- 👉 Export button -->
<VBtn
variant="tonal"
color="secondary"
prepend-icon="tabler-upload"
>
Export
</VBtn>
<VBtn
color="primary"
prepend-icon="tabler-plus"
@click="$router.push('/apps/ecommerce/product/add')"
>
Add Product
</VBtn>
</div>
</div>
<VDivider class="mt-4" />
<!-- 👉 Datatable -->
<VDataTableServer
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:headers="headers"
show-select
:items="products"
:items-length="totalProduct"
class="text-no-wrap"
@update:options="updateOptions"
>
<!-- product -->
<template #item.product="{ item }">
<div class="d-flex align-center gap-x-2">
<VAvatar
v-if="item.image"
size="38"
variant="tonal"
rounded
:image="item.image"
/>
<div class="d-flex flex-column">
<span class="text-body-1 font-weight-medium">{{ item.productName }}</span>
<span class="text-sm text-disabled">{{ item.productBrand }}</span>
</div>
</div>
</template>
<!-- category -->
<template #item.category="{ item }">
<VAvatar
size="30"
variant="tonal"
:color="resolveCategory(item.category)?.color"
class="me-2"
>
<VIcon
:icon="resolveCategory(item.category)?.icon"
size="18"
/>
</VAvatar>
<span class="text-body-1 font-weight-medium">{{ item.category }}</span>
</template>
<!-- stock -->
<template #item.stock="{ item }">
<VSwitch :model-value="item.stock" />
</template>
<!-- status -->
<template #item.status="{ item }">
<VChip
v-bind="resolveStatus(item.status)"
density="default"
label
/>
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<IconBtn>
<VIcon icon="tabler-edit" />
</IconBtn>
<IconBtn>
<VIcon icon="tabler-dots-vertical" />
<VMenu activator="parent">
<VList>
<VListItem
value="download"
prepend-icon="tabler-download"
>
Download
</VListItem>
<VListItem
value="delete"
prepend-icon="tabler-trash"
@click="deleteProduct(item.id)"
>
Delete
</VListItem>
<VListItem
value="duplicate"
prepend-icon="tabler-copy"
>
Duplicate
</VListItem>
</VList>
</VMenu>
</IconBtn>
</template>
<template #bottom>
<VDivider />
<div class="d-flex align-center justify-space-between flex-wrap gap-3 pa-5 pt-3">
<p class="text-sm text-medium-emphasis mb-0">
{{ paginationMeta({ page, itemsPerPage }, totalProduct) }}
</p>
<VPagination
v-model="page"
:length="Math.min(Math.ceil(totalProduct / itemsPerPage), 5)"
:total-visible="$vuetify.display.xs ? 1 : Math.min(Math.ceil(totalProduct / itemsPerPage), 5)"
>
<template #prev="slotProps">
<VBtn
variant="tonal"
color="default"
v-bind="slotProps"
:icon="false"
>
Previous
</VBtn>
</template>
<template #next="slotProps">
<VBtn
variant="tonal"
color="default"
v-bind="slotProps"
:icon="false"
>
Next
</VBtn>
</template>
</VPagination>
</div>
</template>
</VDataTableServer>
</VCard>
</div>
</template>
<style lang="scss" scoped>
.product-widget{
border-block-end: 1px solid rgba(var(--v-theme-on-surface), var(--v-border-opacity));
padding-block-end: 1rem;
}
</style>

10
src/pages/store.vue Executable file
View File

@ -0,0 +1,10 @@
<template>
<div>
<VCard title="Stores 🙌">
<VCardText>This is the Store Page.</VCardText>
<VCardText>
Liste des boutiques
</VCardText>
</VCard>
</div>
</template>

47
src/plugins/1.router/guards.ts Executable file
View File

@ -0,0 +1,47 @@
import type { Router } from 'vue-router'
import { canNavigate } from '@layouts/plugins/casl'
export const setupGuards = (router: Router) => {
// 👉 router.beforeEach
// Docs: https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards
router.beforeEach(to => {
/*
* If it's a public route, continue navigation. This kind of pages are allowed to visited by login & non-login users. Basically, without any restrictions.
* Examples of public routes are, 404, under maintenance, etc.
*/
if (to.meta.public)
return
/**
* Check if user is logged in by checking if token & user data exists in local storage
* Feel free to update this logic to suit your needs
*/
const isLoggedIn = !!(useCookie('userData').value && useCookie('accessToken').value)
/*
If user is logged in and is trying to access login like page, redirect to home
else allow visiting the page
(WARN: Don't allow executing further by return statement because next code will check for permissions)
*/
if (to.meta.unauthenticatedOnly) {
if (isLoggedIn)
return '/'
else
return undefined
}
if (!canNavigate(to)) {
/* eslint-disable indent */
return isLoggedIn
? { name: 'not-authorized' }
: {
name: 'login',
query: {
...to.query,
to: to.fullPath !== '/' ? to.path : undefined,
},
}
/* eslint-enable indent */
}
})
}

10
src/plugins/casl/ability.ts Executable file
View File

@ -0,0 +1,10 @@
import { createMongoAbility } from '@casl/ability'
export type Actions = 'create' | 'read' | 'update' | 'delete' | 'manage'
// ex: Post, Comment, User, etc. We haven't used any of these in our demo though.
export type Subjects = 'Post' | 'Comment' | 'all'
export interface Rule { action: Actions; subject: Subjects }
export const ability = createMongoAbility<[Actions, Subjects]>()

View File

@ -0,0 +1,4 @@
import { useAbility as useCaslAbility } from '@casl/vue'
import type { ability } from '../ability'
export const useAbility = () => useCaslAbility<typeof ability>()

14
src/plugins/casl/index.ts Executable file
View File

@ -0,0 +1,14 @@
import type { App } from 'vue'
import { createMongoAbility } from '@casl/ability'
import { abilitiesPlugin } from '@casl/vue'
import type { Rule } from './ability'
export default function (app: App) {
const userAbilityRules = useCookie<Rule[]>('userAbilityRules')
const initialAbility = createMongoAbility(userAbilityRules.value ?? [])
app.use(abilitiesPlugin, initialAbility, {
useGlobalProperties: true,
})
}

8
src/plugins/casl/shims-ability.d.ts vendored Executable file
View File

@ -0,0 +1,8 @@
import { ability } from './ability';
declare module 'vue' {
interface ComponentCustomProperties {
$ability: typeof ability;
$can(this: this, ...args: Parameters<this['$ability']['can']>): boolean;
}
}

View File

@ -0,0 +1,10 @@
export const genId = <T extends { id: number | string }>(array: T[]) => {
const { length } = array
let lastIndex = 0
if (length)
lastIndex = Number(array[length - 1]?.id) + 1
return lastIndex || (length + 1)
}

View File

@ -0,0 +1 @@
export const paginateArray = (array: unknown[], perPage: number, page: number) => array.slice((page - 1) * perPage, page * perPage)

View File

@ -0,0 +1,6 @@
export const paginationMeta = <T extends { page: number; itemsPerPage: number }>(options: T, total: number) => {
const start = (options.page - 1) * options.itemsPerPage + 1
const end = Math.min(options.page * options.itemsPerPage, total)
return `Showing ${total === 0 ? 0 : start} to ${end} of ${total} entries`
}

46
src/stores/user.store.ts Normal file
View File

@ -0,0 +1,46 @@
interface UserState {
username: string | null
role: string | null
}
export const userStore = defineStore('user', {
state: (): UserState => ({
username: null,
role: null,
}),
actions: {
async login(username: string, password: string, ability: any) {
try {
const res = await $api('http://localhost:8080/hdpos/api/auth/login', {
method: 'POST',
body: {
username,
password,
},
})
const { accessToken, userData, userAbilityRules: { rules } } = res
this.username = username
this.role = password === 'admin123' ? 'Admin' : 'Support'
useCookie('accessToken').value = accessToken
useCookie('userData').value = userData
useCookie('userAbilityRules').value = rules
ability.update(rules)
}
catch (err) {
console.error(err)
}
},
logout() {
this.username = null
this.role = null
// Effacer les cookies
useCookie('accessToken').value = null
useCookie('userData').value = null
useCookie('userAbilityRules').value = null
},
},
})

View File

@ -7,7 +7,8 @@ export const $api = ofetch.create({
if (accessToken) { if (accessToken) {
options.headers = { options.headers = {
...options.headers, ...options.headers,
Authorization: `Bearer ${accessToken}`, 'Authorization': `Bearer ${accessToken}`,
'Cache-Control': 'no-cache',
} }
} }
}, },

View File

@ -1,42 +0,0 @@
<script setup lang="ts">
import { useTheme } from 'vuetify'
const { global } = useTheme()
const authProviders = [
{
icon: 'fa-facebook',
color: '#4267b2',
colorInDark: '#4267b2',
},
{
icon: 'fa-google',
color: '#dd4b39',
colorInDark: '#db4437',
},
{
icon: 'fa-twitter',
color: '#1da1f2',
colorInDark: '#1da1f2',
},
]
</script>
<template>
<div class="d-flex justify-center flex-wrap gap-3">
<VBtn
v-for="link in authProviders"
:key="link.icon"
icon
variant="tonal"
size="38"
:color="global.name.value === 'dark' ? link.colorInDark : link.color"
class="rounded"
>
<VIcon
size="18"
:icon="link.icon"
/>
</VBtn>
</div>
</template>

View File

@ -10,10 +10,10 @@ import { AppContentLayoutNav, ContentWidth, FooterType, NavbarType } from '@layo
export const { themeConfig, layoutConfig } = defineThemeConfig({ export const { themeConfig, layoutConfig } = defineThemeConfig({
app: { app: {
title: 'vuexy', title: 'hdpos',
logo: h('div', { innerHTML: logo, style: 'line-height:0; color: rgb(var(--v-global-theme-primary))' }), logo: h('div', { innerHTML: logo, style: 'line-height:0; color: rgb(var(--v-global-theme-primary))' }),
contentWidth: ContentWidth.Boxed, contentWidth: ContentWidth.Boxed,
contentLayoutNav: AppContentLayoutNav.Vertical, contentLayoutNav: AppContentLayoutNav.Horizontal,
overlayNavFromBreakpoint: breakpointsVuetify.md + 16, // 16 for scrollbar. Docs: https://next.vuetifyjs.com/en/features/display-and-platform/ overlayNavFromBreakpoint: breakpointsVuetify.md + 16, // 16 for scrollbar. Docs: https://next.vuetifyjs.com/en/features/display-and-platform/
i18n: { i18n: {
enable: false, enable: false,

View File

@ -45,6 +45,9 @@
], ],
"@api-utils/*": [ "@api-utils/*": [
"./src/plugins/fake-api/utils/*" "./src/plugins/fake-api/utils/*"
],
"@stores/*": [
"./src/stores/*"
] ]
}, },
"lib": [ "lib": [

1
typed-router.d.ts vendored
View File

@ -43,6 +43,7 @@ declare module 'vue-router/auto/routes' {
'$error': RouteRecordInfo<'$error', '/:error(.*)', { error: ParamValue<true> }, { error: ParamValue<false> }>, '$error': RouteRecordInfo<'$error', '/:error(.*)', { error: ParamValue<true> }, { error: ParamValue<false> }>,
'login': RouteRecordInfo<'login', '/login', Record<never, never>, Record<never, never>>, 'login': RouteRecordInfo<'login', '/login', Record<never, never>, Record<never, never>>,
'second-page': RouteRecordInfo<'second-page', '/second-page', Record<never, never>, Record<never, never>>, 'second-page': RouteRecordInfo<'second-page', '/second-page', Record<never, never>, Record<never, never>>,
'store': RouteRecordInfo<'store', '/store', Record<never, never>, Record<never, never>>,
} }
} }

View File

@ -89,6 +89,7 @@ export default defineConfig({
'apexcharts': fileURLToPath(new URL('node_modules/apexcharts-clevision', import.meta.url)), 'apexcharts': fileURLToPath(new URL('node_modules/apexcharts-clevision', import.meta.url)),
'@db': fileURLToPath(new URL('./src/plugins/fake-api/handlers/', import.meta.url)), '@db': fileURLToPath(new URL('./src/plugins/fake-api/handlers/', import.meta.url)),
'@api-utils': fileURLToPath(new URL('./src/plugins/fake-api/utils/', import.meta.url)), '@api-utils': fileURLToPath(new URL('./src/plugins/fake-api/utils/', import.meta.url)),
'@stores': fileURLToPath(new URL('./src/stores/', import.meta.url)),
}, },
}, },
build: { build: {