Skip to content

Commit

Permalink
Merge pull request #30 from doroudi/doroudi/issue27
Browse files Browse the repository at this point in the history
  • Loading branch information
doroudi authored Nov 19, 2023
2 parents 3378953 + 3d0d7d1 commit d32913f
Show file tree
Hide file tree
Showing 14 changed files with 459 additions and 25 deletions.
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

0 comments on commit d32913f

Please sign in to comment.