This commit is contained in:
mohadese namavar
2024-06-16 00:22:14 +04:30
commit ec84dfd222
322 changed files with 77942 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="become-seller-form">
<h1 v-if="device == 'desktop'">
{{ $t("becomeSeller") }}
</h1>
<div>
<ul>
<li v-for="(item, key) in fields" :key="key">
<component :id="key" :is="item.is" v-bind="item.props" v-model="form[key]" />
<label :for="key"> {{ $t(key) }} </label>
<small v-if="errors[key]">{{ errors[key] }}</small>
</li>
</ul>
<div>
<Button v-bind="btns.cancel.props" @click="$router.go(-1)" />
<Button v-bind="btns.save.props" @click="handleClick()" />
</div>
</div>
</main>
</NuxtLayout>
</template>
<script setup>
import init from '../../../initialize/become-seller'
const { btns, fields } = init()
const { device, toast, t } = inject('service')
const config = reactive({
mobile: { header: { back: true, title: t('becomeSeller') }, navigation: false },
})
const router = useRouter()
const errors = ref({})
const form = reactive({})
const { status, data, error } = await useFetch('/api/users/current', {
headers: {
Authorization: useCookie('token')
}
})
if (status.value == 'success') {
provide('data', data.value)
}
else throw createError(error.value)
const handleClick = async () => {
errors.value = {}
btns.save.props.loading = true
if (form.nationalCard) {
const formData = new FormData()
formData.append('images', form.nationalCard)
const upload = await useFetch('/api/images/upload', {
headers: { Authorization: useCookie('token') },
params: { type: 3 },
method: 'post',
body: formData,
})
if (upload.status.value == 'success') {
const body = Object.assign({}, form)
body.nationalCard = upload.data.value.urls[0]
const { status, error } = await useFetch('/api/users/seller', {
headers: { Authorization: useCookie('token') },
method: 'put',
body
})
if (status.value == 'success') {
toast.add({
life: 5000, severity: 'success', summary: t('success'), detail: t('becomeSellerSuccess')
})
router.push('/panel/profile')
} else {
if (error.value.statusCode == 422)
errors.value = error.value.data
else
toast.add({
life: 5000, severity: 'error', summary: t('error'), detail: error.value.data.message
})
}
} else {
toast.add({
life: 3000, severity: 'error', summary: t('error'), detail: upload.error.value.message
})
}
} else
errors.value.nationalCard = t('noChooseNationalCardImage');
btns.save.props.loading = false
}
watch(form, () => errors.value = {})
</script>
<style lang="scss">
.become-seller-form {
@apply w-full flex flex-col items-center gap-12 pb-20 max-lg:pt-9;
h1 {
@apply w-full h-[5.9rem] bg-[#F2F2F2] flex items-center justify-center;
@apply text-[#333333] text-2xl font-bold font-iran-sans;
}
&>div {
@apply flex flex-col gap-12;
ul {
@apply grid lg:grid-cols-2 gap-6;
li {
@apply w-full sm:w-[27.1rem] h-max relative;
label {
@apply w-max absolute right-3 top-4 px-1 bg-white transition-['top'] select-none;
@apply text-neutral-400 text-base font-medium font-vazir cursor-text pointer-events-none;
}
input {
@apply h-14;
}
input,
textarea {
@apply w-full rtl rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
.p-inputnumber input {
@apply ltr;
}
&:has(img),
&:has(.p-inputwrapper-filled),
&:has(input:focus),
&:has(textarea:focus),
&:has(input:not(:placeholder-shown):not([hidden])),
&:has(textarea:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal font-vazir;
}
}
.p-dropdown,
.p-inputnumber,
.p-button {
@apply w-full;
}
&:nth-child(7) {
@apply row-span-3 h-full;
}
small {
@apply text-red-500 font-vazir;
}
&:has(small) * {
@apply border-red-500 #{!important};
}
}
}
&>div {
@apply inset-x-0 bottom-0 flex gap-3 w-full lg:w-[27.1rem] self-end;
@apply max-lg:fixed max-lg:border-t max-lg:shadow-[0_-2px_5px_#0000000a];
@apply h-[4.8rem] px-6 pt-3 lg:p-0 lg:h-max bg-white;
&>button {
@apply w-full h-[2.9rem] lg:h-14 #{!important};
.p-button-label {
@apply text-sm lg:text-xl font-medium font-vazir;
}
}
}
}
}
</style>
+161
View File
@@ -0,0 +1,161 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="seller-new-product">
<aside v-if="device == 'desktop'">
<PanelSellerProfile />
<PanelSellerScore />
</aside>
<section>
<div>
<template v-for="i in 3" :key="i">
<span v-text="i" :class="{ 'active': i == step, 'past': i < step }" />
<hr :class="{ 'past': i < step }" />
</template>
</div>
<Transition name="fade" mode="out-in">
<PanelSellerStepFirst v-if="step == 1" />
<PanelSellerStepSecond v-else-if="step == 2" />
<PanelSellerStepThirdy v-else-if="step == 3" />
</Transition>
</section>
</main>
</NuxtLayout>
</template>
<script setup>
import init from '../../../initialize/new-product.js'
provide('init', init())
const { device, t } = inject('service')
const config = reactive({
desktop: { toolbar: true, seller: true },
mobile: { header: { back: true, toolbar: true, title: t('userPanel') }, navigation: false },
})
const { query } = useRoute()
const step = ref(1)
provide('step', step)
const { status, data, error } = await useFetch('/api/shop/newProduct', {
headers: {
Authorization: useCookie('token')
},
})
if (status.value == 'success')
provide('data', data.value)
else throw createError(error.value)
const form = ref({
specs: {
[t("weight")]: "",
[t("dimensions")]: "",
[t("color")]: "",
},
images: [null]
})
if (query.id) {
const { status, data } = await useFetch(`/api/products/id/${query.id}`, {
headers: {
Authorization: useCookie('token')
},
})
if (status.value == 'success') {
form.value = data.value
form.value.subCategory =
form.value.category.length > 1 ? form.value.category.slice(-1)[0] : null
form.value.category =
form.value.category.length > 2 ? form.value.category[1] : form.value.category[0]
}
}
provide('form', form.value)
</script>
<style lang="scss">
.seller-new-product {
@apply container flex pt-[1.9rem] pb-4 lg:pt-14 max-lg:px-6 gap-6 relative;
aside {
@apply flex flex-col gap-3 lg:gap-5;
}
&>section {
@apply flex flex-col gap-9 grow;
&>div:not([class]) {
@apply w-full h-max flex justify-between items-center gap-2;
hr {
@apply grow h-0.5 bg-[#333333] last:hidden;
}
hr.past {
@apply bg-[#47B556];
}
span {
@apply w-6 h-6 lg:w-[1.9rem] lg:h-[1.9rem] rounded-full flex justify-center items-center;
@apply text-xs lg:text-base font-medium font-vazir border border-[#333333];
}
span.past {
@apply text-white bg-[#47B556] border-[#47B556];
}
span.active {
@apply border-[#47B556] text-[#47B556];
}
}
}
}
body:has(.seller-new-product) {
.p-dropdown-panel:has(.p-dropdown-filter-container) {
@apply w-[18.3rem] translate-y-3 pb-4 pt-[0.6rem] rounded-[0.6rem];
.p-dropdown-header {
@apply py-0 px-2.5;
.p-dropdown-filter-container {
@apply w-full h-[3.1rem];
input {
@apply m-0 h-full w-full rtl;
}
svg {
@apply hidden;
}
}
}
.p-dropdown-items-wrapper {
@apply h-[19rem] mt-3 ml-3;
.p-dropdown-item-group {
@apply text-[#47B556] text-xs font-medium font-vazir p-2.5;
}
.p-dropdown-item {
@apply h-[2.4rem] px-2.5 text-[#333333] text-base font-medium font-vazir;
}
&::-webkit-scrollbar-thumb {
@apply bg-[#47B556];
}
&::-webkit-scrollbar {
@apply w-1;
}
}
}
}
</style>
+56
View File
@@ -0,0 +1,56 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="seller-orders">
<aside v-if="device == 'desktop'">
<PanelSellerProfile />
<PanelSellerScore />
</aside>
<ul>
<li v-for="(order, i) in orders" :key="i">
<PanelSellerOrder :data="order" />
</li>
<li v-if="orders.length < 1">
<p>{{ $t('notFound') }}</p>
</li>
</ul>
</main>
</NuxtLayout>
</template>
<script setup>
const { device, t } = inject('service')
const config = reactive({
desktop: { toolbar: true, seller: true },
mobile: { header: { back: true, toolbar: true, title: t('userPanel') } },
})
const { meta, query } = useRoute()
const orders = ref([])
const { status, data, error } = await useFetch('/api/shop/orders', {
headers: {
Authorization: useCookie('token')
}
})
if (status.value == 'success') {
orders.value = data.value.shop.pendingProducts
provide('data', data.value)
}
else throw createError(error.value)
</script>
<style lang="scss">
.seller-orders {
@apply container flex pt-[1.9rem] pb-20 lg:pt-14 max-lg:px-6 gap-6;
&>aside {
@apply flex flex-col gap-3 lg:gap-5;
}
&>ul {
@apply w-full flex flex-col gap-3 lg:gap-2.5;
}
}
</style>
+294
View File
@@ -0,0 +1,294 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="seller-products">
<aside v-if="device == 'desktop'">
<PanelSellerProfile />
<PanelSellerScore />
</aside>
<DataTable :value="products" :responsiveLayout="rl">
<template #header>
<div>
<IconField>
<InputText v-model="form.q" :placeholder="$t('search')" @keypress.enter="filter()" />
<InputIcon class="isax isax-search-normal-1" />
</IconField>
<Dropdown v-model="form.category" :placeholder="$t('selectCategory')" showClear filter
:options="categories.items" optionLabel="name" optionGroupLabel="name"
optionGroupChildren="sub" optionValue="_id" @change="filter()">
<template #dropdownicon>
<i class="isax isax-arrow-bottom" />
</template>
</Dropdown>
</div>
<span> {{ $t('allProducts') }}: {{ products.length }}</span>
</template>
<Column v-for="(col, i) in columns[device]" :key="i" v-bind="col">
<template #body="{ data, field }">
<span>{{ data[field] || field(data) }}</span>
</template>
</Column>
<Column>
<template #body="{ data, index }">
<router-link :to="`/panel/seller/new-product?id=${data.id}`">
<Button icon="isax isax-edit" rounded link severity="secondary" />
</router-link>
<Button icon="isax isax-trash" rounded link severity="secondary" :loading="data.removing"
@click="remove(data, index)" />
<Button v-if="device == 'mobile'" :label="$t('showDetails')" icon="isax isax-arrow-left-2"
rounded link severity="secondary" />
</template>
</Column>
<template #empty>
<p>{{ $t('notFound') }}</p>
</template>
</DataTable>
</main>
</NuxtLayout>
</template>
<script setup>
const { device, t, toast } = inject('service')
const config = reactive({
desktop: { toolbar: true, seller: true },
mobile: { header: { back: true, toolbar: true, title: t('userPanel') } },
})
const router = useRouter()
const categories = useCategoriesStore()
const rl = computed(() => device.value == 'mobile' ? 'stack' : '')
const { meta, query, path } = useRoute()
const products = ref([])
const columns = reactive({
desktop: [
{ header: t('row'), field: (p) => products.value.indexOf(p) + 1 },
{ header: t('code'), field: 'uid' },
{ header: t('productTitle'), field: 'title' },
{ header: t('salesPrice'), field: (p) => numberFormat(p.price) },
{ header: t('inventory'), field: (p) => numberFormat(p.stockQuantity) },
{ header: t('status'), field: (p) => [t('pending'), t('confirmed'), t('rejected')][p.status] },
],
mobile: [
{ field: 'title' },
]
})
const { status, data, error } = await useFetch('/api/shop/products', {
headers: {
Authorization: useCookie('token')
},
params: query
})
if (status.value == 'success') {
products.value = data.value.shop.products
provide('data', data.value)
}
else throw createError(error.value)
const form = reactive(Object.assign({}, query))
const filter = () => {
Object.keys(form).forEach((key) => {
if (!form[key]) delete form[key]
})
router.push({ path, query: form })
}
const remove = async (data, index) => {
data.removing = true
const { status, error } = await useFetch('/api/products/' + data.id, {
headers: {
Authorization: useCookie('token')
},
method: "delete", body: {}
})
data.removing = false
if (status.value == 'success') {
products.value.splite(index, 1)
} else toast.add({
life: 3000, severity: 'error', summary: t('error'), detail: error.value.data.message
})
}
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.seller-products {
@apply container flex flex-col lg:flex-row pt-[1.9rem] pb-20 lg:pt-14 max-lg:px-6 gap-6;
aside {
@apply flex flex-col gap-3 lg:gap-5;
}
.p-datatable {
@apply flex flex-col gap-4 lg:gap-11 rtl grow;
.p-datatable-header {
@apply flex max-lg:flex-col gap-9 lg:items-center bg-transparent p-0 border-none z-[1];
&>div {
@apply flex max-lg:flex-col gap-3 lg:gap-[1.1rem];
.p-icon-field {
@apply z-0 flex items-center w-[21.4rem] h-[2.9rem] lg:w-[29.7rem] lg:h-[3.8rem] rounded-[0.6rem];
@apply bg-[#FAFAFA] lg:bg-[#F2F2F2] text-[#4C4C4C] lg:text-[#5D5D5D];
@apply text-sm lg:text-lg font-medium font-vazir;
input {
@apply rtl w-full h-full border-none bg-transparent shadow-none;
}
}
.p-dropdown {
@apply items-center w-[21.4rem] h-[2.9rem] lg:w-[18.3rem] lg:h-[3.8rem] bg-[#FAFAFA] lg:bg-[#F2F2F2] rounded-[0.6rem] border-none;
@apply z-[2] #{!important};
.p-dropdown-label {
@apply text-[#4C4C4C] lg:text-[#5D5D5D] text-xs lg:text-base font-medium font-vazir mt-3.5 lg:mt-1.5;
}
i {
@apply text-xl lg:text-2xl transition-transform;
}
&.p-overlay-open i {
@apply rotate-180;
}
.p-dropdown-clear-icon {
@apply left-12 right-auto;
}
}
&:has(.p-overlay-open)::after {
@apply content-['_'] inset-0 fixed bg-black/20 z-[1];
}
}
&>span {
@apply text-[#333333] text-sm lg:text-xl font-medium font-vazir mr-auto;
}
}
.p-datatable-table {
@apply border-separate border-spacing-y-[0.8rem] px-1;
.p-datatable-thead {
@apply w-full h-[3.1rem] max-lg:hidden;
th {
@apply text-[#5D5D5D] text-base font-normal font-vazir bg-[#F2F2F2];
@apply first:rounded-r-[0.6rem] last:rounded-l-[0.6rem];
}
}
.p-datatable-tbody {
@apply lg:translate-y-1.5;
tr {
@apply w-full rounded-[0.6rem] bg-transparent shadow-[0px_0px_6px_#0000001f];
td {
@apply text-[#333333] text-base font-normal font-vazir text-right;
@apply first:rounded-r-[0.6rem] last:rounded-l-[0.6rem] py-0 h-[3.8rem] last:text-left;
&:last-child:not([colspan]) {
@apply flex lg:justify-end gap-3 items-center;
.p-button {
@apply p-0 w-max h-max #{!important};
.p-button-icon {
@apply text-lg lg:text-2xl text-[#333333];
}
}
}
&>span {
@apply max-lg:py-3 min-h-[3.8rem] flex items-center text-xs lg:text-base text-[#333333] font-vazir;
}
@media(max-width: 1024px) {
&>button:last-child {
@apply -m-3 mr-auto #{!important};
.p-button-label {
@apply text-xs font-medium font-vazir whitespace-nowrap #{!important};
}
.p-button-icon {
@apply mr-px text-[#47B556] #{!important};
}
}
&:first-child span {
@apply w-full border-b py-3;
}
.p-column-title {
@apply hidden #{!important};
}
}
}
}
.p-datatable-emptymessage p {
@apply lg:h-[3.8rem] flex justify-center items-center
}
}
}
}
}
body:has(.seller-products) {
.p-dropdown-panel {
@apply w-[18.3rem] h-[19rem] translate-y-3 pb-4 pt-[0.6rem] rounded-[0.6rem];
.p-dropdown-header {
@apply py-0 px-2.5;
.p-dropdown-filter-container {
@apply w-[17rem] h-[3.1rem];
input {
@apply m-0 h-full rtl;
}
svg {
@apply hidden;
}
}
}
.p-dropdown-items-wrapper {
@apply mt-3 ml-3;
.p-dropdown-item-group {
@apply text-[#47B556] text-xs font-medium font-vazir p-2.5;
}
.p-dropdown-item {
@apply h-[2.4rem] px-2.5 text-[#333333] text-base font-medium font-vazir;
}
&::-webkit-scrollbar-thumb {
@apply bg-[#47B556];
}
&::-webkit-scrollbar {
@apply w-1;
}
}
}
}
</style>
+76
View File
@@ -0,0 +1,76 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="seller-store">
<aside>
<PanelSellerProfile />
<PanelSellerScore />
</aside>
<section>
<div>
<PanelSellerMangement title="warehouse" />
<PanelSellerMangement title="orders" />
<img src="/images/sell.svg">
</div>
<PanelSellerUpcoming />
<div>
<PanelSellerSales title="status" />
<PanelSellerSales title="number" />
<PanelSellerPieChart />
</div>
<PanelSellerBestSelling />
</section>
</main>
</NuxtLayout>
</template>
<script setup>
const { device, t } = inject('service')
const config = reactive({
desktop: { toolbar: true, seller: true },
mobile: { header: { back: true, toolbar: true, title: t('userPanel') } },
})
const { meta, query } = useRoute()
const { status, data, error } = await useFetch('/api/shop', {
headers: {
Authorization: useCookie('token')
}
})
if (status.value == 'success') {
provide('data', data.value)
}
else throw createError(error.value)
</script>
<style lang="scss">
.seller-store {
@apply w-full container flex flex-col lg:flex-row pt-[1.9rem] pb-20 lg:pt-14 max-lg:px-6 gap-6 justify-center;
aside {
@apply flex max-lg:flex-wrap max-lg:justify-between lg:flex-col gap-3 lg:gap-5;
}
&>section {
@apply flex flex-col gap-6 lg:gap-[1.9rem];
&>div:nth-child(1) {
@apply flex max-lg:flex-wrap gap-3 max-lg:justify-between lg:gap-6;
img {
@apply object-cover object-[0%_75%] w-[21.4rem] h-[18.1rem] lg:w-[18.3rem] lg:h-[25.2rem] rounded-[0.6rem];
@apply max-xl:hidden;
}
}
&>div:nth-child(3) {
@apply flex max-lg:flex-wrap gap-3 max-lg:justify-between lg:gap-6;
}
}
}
</style>