Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Doroudi/issue27 #30

Merged
merged 2 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,19 @@ categories:
validations:
nameRequired: Name is required
parentRequired: Parent Not Selected

brands:
title: Brands
createButton: Create
create:
buttonTitle: Create
title: Create New Brand
image: Image
brandName: Name
url: Url
validations:
name: Name is required
url: Url is required
image: Image is required

not-found: Not found
3 changes: 3 additions & 0 deletions src/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ declare global {
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBrandStore: typeof import('./store/brand.store')['useBrandStore']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
Expand Down Expand Up @@ -444,6 +445,7 @@ declare module 'vue' {
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBrandStore: UnwrapRef<typeof import('./store/brand.store')['useBrandStore']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
Expand Down Expand Up @@ -747,6 +749,7 @@ declare module '@vue/runtime-core' {
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBrandStore: UnwrapRef<typeof import('./store/brand.store')['useBrandStore']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
Expand Down
4 changes: 4 additions & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
BarChart: typeof import('./components/BarChart.vue')['default']
BrandManagement: typeof import('./components/Brand/BrandManagement.vue')['default']
Card: typeof import('./components/Card.vue')['default']
CategoryManagement: typeof import('./components/Category/CategoryManagement.vue')['default']
CategoryStatics: typeof import('./components/Category/CategoryStatics.vue')['default']
CreateBrand: typeof import('./components/Brand/CreateBrand.vue')['default']
CreateCategory: typeof import('./components/Category/CreateCategory.vue')['default']
DashboardCard: typeof import('./components/DashboardCard.vue')['default']
DonutChart: typeof import('./components/DonutChart.vue')['default']
Expand All @@ -33,10 +35,12 @@ declare module 'vue' {
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPageHeader: typeof import('naive-ui')['NPageHeader']
NPopselect: typeof import('naive-ui')['NPopselect']
NTreeSelect: typeof import('naive-ui')['NTreeSelect']
NUpload: typeof import('naive-ui')['NUpload']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Sidebar: typeof import('./components/Sidebar.vue')['default']
Expand Down
133 changes: 133 additions & 0 deletions src/components/Brand/BrandManagement.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<script setup lang='ts'>
import { type DataTableColumns, NButton, NIcon } from 'naive-ui/es/components'
import type { RowData } from 'naive-ui/es/data-table/src/interface'
import {
DismissCircle24Regular as DeleteIcon,
Edit32Regular as EditIcon,
AddCircle20Regular as PlusIcon,
} from '@vicons/fluent'
import { storeToRefs } from 'pinia'
import { useDialog, useMessage } from 'naive-ui'

const { t } = useI18n()
const store = useBrandStore()
const { brands, isLoading } = storeToRefs(store)
const dialog = useDialog()
const message = useMessage()
onMounted(getItems)
const columns: DataTableColumns<RowData> = [
{
title: 'Name',
key: 'name',
},
{
title: 'Url Slog',
key: 'url',
},
{
title: 'Actions',
key: 'actions',
width: 200,
render(row) {
return [
h(
NButton,
{
size: 'small',
renderIcon: renderIcon(EditIcon),
ghost: true,
class: 'mr-2',
onClick: () => edit(row),
},
{ default: () => 'Edit' },
),
h(
NButton,
{
size: 'small',
type: 'error',
ghost: true,
renderIcon: renderIcon(DeleteIcon),
onClick: () => handleDeleteItem(row),
},
{ default: () => 'Delete' },
),
]
},
},
]
const { options } = storeToRefs(store)
const showAddDialog = ref(false)
function renderIcon(icon: any) {
return () => h(NIcon, null, { default: () => h(icon) })
}

function handleDeleteItem(row: RowData) {
dialog.error({
title: 'Confirm',
content: 'Are you sure?',
positiveText: 'Yes, Delete',
negativeText: 'Cancel',
onPositiveClick: () => {
store.deleteBrand(row.id)
message.success('Brand was deleted!')
},
})
}

function rowKey(row: RowData) {
return row.id
}
function getItems() {
store.getBrands(options.value)
}

function handlePageChange(page: number) {
options.value.page = page
getItems()
}

function handleFiltersChange() {
getItems()
}

function createBrand() {
showAddDialog.value = true
}
</script>

<template>
<n-layout>
<n-layout-content>
<div>
<div class="flex items-center mb-5">
<h1 class="page-title mx-2">
{{ t('brands.title') }}
</h1>
<NButton type="primary" quaternary round @click="createBrand">
<template #icon>
<NIcon>
<PlusIcon />
</NIcon>
</template>
{{ t('brands.createButton') }}
</NButton>
</div>
<n-data-table
remote :columns="columns" :data="brands" :loading="isLoading" :pagination="options"
:row-key="rowKey" @update:sorter="handleSorterChange" @update:filters="handleFiltersChange"
@update:page="handlePageChange"
/>
</div>
</n-layout-content>

<n-drawer v-model:show="showAddDialog" :width="502" placement="right">
<n-drawer-content closable title="Create Brand">
<CreateBrand @close="showAddDialog = false" />
</n-drawer-content>
</n-drawer>
</n-layout>
</template>

<style scoped lang='scss'>
</style>
103 changes: 103 additions & 0 deletions src/components/Brand/CreateBrand.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<script setup lang='ts'>
import type { FormInst, FormRules } from 'naive-ui/es/form'
import { storeToRefs } from 'pinia'
import type { CategoryCreateModel } from '~/models/Category'

const emits = defineEmits(['close'])
const brandStore = useBrandStore()
const { isLoading } = storeToRefs(brandStore)
const brandItem = ref<CategoryCreateModel>({ name: '', parentId: 0 })
const { t } = useI18n()
const formRef = ref<FormInst | null>(null)
async function create() {
formRef.value?.validate(async (errors: any) => {
if (!errors) {
await brandStore.createBrand(brandItem.value)
emits('close')
}
})
}

const nameInput = ref()
onMounted(() => {
nameInput.value?.focus()
})

const rules: FormRules = {
name: [
{
required: true,
trigger: ['blur', 'change'],
message: t('brands.validations.name'),
},
],
url: [
{
required: true,
trigger: ['blur', 'change'],
message: t('brands.validations.url'),
},
],
image: [
{
required: true,
trigger: ['blur', 'change'],
message: t('brands.validations.image'),
},
],
}

const showModalRef = ref(false)
const previewImageUrlRef = ref('')
function handlePreview(file: UploadFileInfo) {
const { url } = file
previewImageUrlRef.value = url as string
showModalRef.value = true
}
</script>

<template>
<n-form ref="formRef" :model="brandItem" :rules="rules" @submit.prevent="create()">
<div class="form-control">
<n-form-item class="mb-5" path="name" :label="t('brands.create.brandName')">
<n-input
id="name" ref="nameInput" v-model:value="brandItem.name" autofocus
:placeholder="t('brands.create.brandName')"
/>
</n-form-item>
</div>
<div class="form-control flex flex-col mb-5">
<n-form-item path="url" :label="t('brands.create.url')">
<n-input
v-model:value="brandItem.url" autofocus
:placeholder="t('brands.create.url')"
/>
</n-form-item>
</div>

<div class="form-control">
<n-form-item class="mb-5" path="image" :label="t('brands.create.image')">
<n-upload
:default-file-list="previewFileList"
list-type="image-card"
accept="image/png, image/jpeg"
max="1"
@preview="handlePreview"
/>
<n-modal
v-model:show="showModal"
preset="card"
style="width: 600px"
title="A Cool Picture"
>
<img :src="previewImageUrl" style="width: 100%">
</n-modal>
</n-form-item>
</div>
<n-button attr-type="submit" size="large" :block="true" type="primary" :loading="isLoading">
{{ t('brands.create.buttonTitle') }}
</n-button>
</n-form>
</template>

<style scoped lang='scss'></style>
7 changes: 2 additions & 5 deletions src/components/Category/CategoryManagement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const columns: DataTableColumns<RowData> = [
renderIcon: renderIcon(EditIcon),
ghost: true,
class: 'mr-2',
onClick: () => edit(row),
onClick: () => {},
},
{ default: () => 'Edit' },
),
Expand All @@ -59,14 +59,11 @@ const columns: DataTableColumns<RowData> = [
]
const { options } = storeToRefs(store)
const showAddDialog = ref(false)

function renderIcon(icon: any) {
return () => h(NIcon, null, { default: () => h(icon) })
}

function edit(row: RowData) {

}

function handleDeleteItem(row: RowData) {
dialog.error({
title: 'Confirm',
Expand Down
59 changes: 59 additions & 0 deletions src/mocks/handlers/brand.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { rest } from 'msw'
import _ from 'lodash'
import { faker } from '@faker-js/faker'
import { CreatePagedResponse } from '../handlers.utility'
import type { Brand, BrandCreateModel } from '~/models/Brand'

const brands = _.times(7, createFakeBrand)
const handlers = [
rest.get('/api/Brand', (req, res, ctx) => {
const response = CreatePagedResponse<Brand>(req, brands)
return res(
ctx.status(200),
ctx.delay(200),
ctx.json(response),
)
}),
rest.post('/api/brand', async (req, res, ctx) => {
const newItem = await req.json<BrandCreateModel>()
const brand: Brand = {
id: faker.datatype.number({ max: 2000 }).toString(),
name: newItem.name,
url: newItem.url,
image: newItem.image,
}
brands.push(brand)
return res(
ctx.status(200),
ctx.delay(200),
ctx.json(brand),
)
}),
rest.delete('/api/Brand/:id', (req, res, ctx) => {
const id = req.params.id.toString()
const itemIndex = brands.findIndex(x => x.id === id)
brands.splice(itemIndex, 1)
return res(
ctx.delay(1000),
ctx.status(200),
ctx.json(true),
)
}),

]

function createFakeBrand(): Brand {
const name = faker.company.name()
return {
id: faker.datatype.number().toString(),
name,
image: '',
url: toKebabCase(name),
}
}

function toKebabCase(str: string) {
return str.replaceAll(' ', '').replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
}

export default handlers
Loading
Loading