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
+149
View File
@@ -0,0 +1,149 @@
<template>
<NuxtLayout name="auth" aside="LoginForm">
<form class="login">
<h1>{{ $t('loginAndRegister') }}</h1>
<ul>
<li v-for="(item, key) in fields" :key="key">
<component :is="item.is" v-bind="item.props" v-model="form[key]" />
<label :for="key" v-text="$t(key)" />
</li>
</ul>
<div>
<div>
<Checkbox v-model="form.remember" v-bind="checkbox.props" />
<label for="remember" v-text="$t('rememberMe')" />
</div>
<div>
<span>{{ $t('forgetPassword') }}</span>
<router-link to="/auth/restore">
{{ $t('restorePassword') }}
</router-link>
</div>
</div>
<div>
<Button v-bind="btns.login.props" @click="login()" />
<router-link to="/auth/register">
<Button v-bind="btns.register.props" />
</router-link>
</div>
</form>
</NuxtLayout>
</template>
<script setup>
import init from '../../initialize/auth/login'
const { fields, checkbox, btns } = init()
const { toast, t } = inject('service')
const form = reactive({})
const route = useRoute()
const router = useRouter()
const store = useUserStore()
const login = async () => {
btns.login.props.loading = true
const { status, error } = await store.login(form)
btns.login.props.loading = false
if (status.value == 'success')
router.push(route.query.redirect || '/panel/profile')
else {
let detail = error.value.data.msg
if (error.value.statusCode == 422)
detail = Object.values(error.value.data).join('\n')
toast.add({ life: 3000, severity: 'error', summary: t('error'), detail })
}
}
</script>
<style lang="scss">
.auth .login {
@apply flex flex-col h-max m-auto;
@apply gap-6 lg:gap-10;
&>h1 {
@apply text-[#CCCCCC] font-black font-vazir text-right leading-[3rem] whitespace-nowrap;
@apply text-xl lg:text-3xl;
}
&>ul {
@apply flex flex-col gap-6 mt-1.5;
li {
@apply h-14 relative;
label {
@apply w-max absolute right-3 top-4 px-1 bg-white transition-['top'];
@apply text-neutral-400 text-base font-medium cursor-text font-vazir;
}
input {
@apply w-full rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
.p-password {
svg {
@apply text-[#CCCCCC] w-5 h-5 -mt-2.5;
}
}
&:has(input:focus),
&:has(input:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal font-vazir;
}
}
}
}
&>div:nth-of-type(1) {
@apply flex font-vazir whitespace-nowrap text-[0.7em] items-center justify-between;
@apply lg:mt-5 lg:text-sm;
&>div {
@apply flex items-center gap-1;
.p-checkbox,
.p-checkbox-box {
@apply w-3.5 h-3.5 lg:w-[1.1rem] lg:h-[1.1rem];
}
label,
span {
@apply text-neutral-400 font-medium;
}
label {
@apply max-lg:ml-12 mr-1 cursor-pointer;
}
span {
@apply mr-auto;
}
a {
@apply text-blue-500 font-bold;
}
}
}
&>div:last-of-type {
@apply flex w-full justify-center gap-2 mt-[1.9rem] lg:gap-4 lg:mt-4;
a {
@apply w-full flex;
}
button {
@apply w-full h-11 lg:h-14 #{!important};
.p-button-label {
@apply text-sm font-medium font-iran-sans;
}
}
}
}
</style>
+18
View File
@@ -0,0 +1,18 @@
<template>
<NuxtLayout name="auth" :aside="asides[step]" :steps="4">
<AuthPhone v-if="step == 0" />
<AuthCode v-else-if="step == 1" />
<AuthPassword v-else-if="step == 2" />
<AuthInformation v-else-if="step == 3" />
</NuxtLayout>
</template>
<script setup>
const route = useRoute()
const { step = 0 } = route.query
const asides = reactive(['EnterPhoneNumber', 'EnterOTPCode', 'ChoosePassword', 'CompleteInformation'])
</script>
<style lang="scss" scoped>
</style>
+18
View File
@@ -0,0 +1,18 @@
<template>
<NuxtLayout name="auth" :aside="asides[step]" :steps="3">
<AuthPhone v-if="step == 0" />
<AuthCode v-else-if="step == 1" />
<AuthPassword v-else-if="step == 2" />
<AuthInformation v-else-if="step == 3" />
</NuxtLayout>
</template>
<script setup>
const route = useRoute()
const { step = 0 } = route.query
const asides = reactive(['EnterPhoneNumber', 'EnterOTPCode', 'ChoosePassword'])
</script>
<style lang="scss" scoped>
</style>
+38
View File
@@ -0,0 +1,38 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="blog">
<BlogCategories v-if="device == 'desktop'" />
<BlogList />
<BlogAside />
</main>
</NuxtLayout>
</template>
<script setup>
const config = reactive({
mobile: { header: { search: true, menu: true }, navigation: false },
})
const { sidebar, device } = inject('service')
const route = useRoute()
const { page = 1, ...params } = route.query
const { status, data, error } = await useFetch(`/api/posts/${page}`, { params })
if (status.value == 'success') {
sidebar.is = resolveComponent('BlogCategories')
sidebar.props = { data }
useSeoMeta(data.value.meta || {})
provide('data', data.value)
}
else throw createError(error.value)
</script>
<style lang="scss">
.blog {
@apply w-full mx-auto flex flex-col gap-12 lg:gap-6 py-[1.9rem] lg:pt-12 lg:pb-[7.1rem];
@apply lg:flex-row lg:px-6 justify-around 2xl:justify-start 2xl:items-start container 2xl:max-w-[120rem];
@apply min-[1725px]:justify-between;
}
</style>
+79
View File
@@ -0,0 +1,79 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="blog-post">
<AppBreadcrumbs :items="breadcrumbs" />
<section>
<BlogContent />
<div>
<BlogShare />
<BlogTags />
</div>
<BlogAside />
</section>
<BlogRelateds />
</main>
</NuxtLayout>
</template>
<script setup>
definePageMeta({ path: '/blog/:link' })
const config = reactive({
mobile: { header: { search: true, menu: true }, navigation: false },
})
const { device, t } = inject('service')
const breadcrumbs = reactive([
{ name: t('asanMarketOnlineStore'), link: '/' },
{ name: t('media'), link: '/blog' },
])
const route = useRoute()
const link = route.params.link
const { status, data, error } = await useFetch(`/api/posts/link/${link}`)
if (status.value == 'success') {
const { sidebar } = inject('service')
sidebar.is = resolveComponent('BlogCategories')
sidebar.props = { data }
const post = data.value.post
breadcrumbs.push(...[
{ name: post.category.name, link: '/blog?category=' + post.category.link },
{ name: post.title, link: route.path }
])
useSeoMeta(data.value.meta || {})
provide('data', data.value)
}
else throw createError(error.value)
</script>
<style lang="scss">
.blog-post {
@apply w-full container flex flex-col gap-[1.1rem];
@apply lg:gap-12 pb-8 pt-[1.1rem] lg:pt-14 lg:pb-[7.5rem];
&>section:not([class]) {
@apply w-full flex flex-wrap flex-col lg:flex-row gap-12 justify-between;
&>div {
@apply w-full flex flex-col lg:flex-row-reverse justify-between border-[#E5E5E5];
@apply gap-5 lg:gap-0 lg:items-center lg:pb-8 lg:border-b-2 max-lg:mt-1.5;
@apply mt-1.5 lg:mt-[4.5rem] lg:order-last;
}
aside {
@apply w-auto container;
&>div:nth-child(2) {
@apply max-lg:hidden;
}
}
}
.blog-relateds {
@apply mt-6 lg:mt-4;
}
}
</style>
+175
View File
@@ -0,0 +1,175 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="categories">
<div>
<ul>
<li v-for="(item, i) in items" :key="i" :active="sub == i">
<div @click="sub = i" v-ripple>
<i :class="'isax ' + item.icon"></i>
<span>{{ item.name }}</span>
</div>
</li>
</ul>
</div>
<div>
<template v-if="items[sub]?.sub?.length > 0">
<router-link :to="{ path: '/search', query: { category: items[sub].name } }">
<Button :label="$t('allProductsOf', items[sub])" link icon="isax isax-arrow-left-2" />
</router-link>
</template>
<Accordion expand-icon="isax left" collapse-icon="isax up">
<template v-for="(item, i) in items[sub]?.sub" :key="i">
<AccordionTab :header="item.name">
<ul>
<li v-for="(cat, i) in item?.sub" :key="i">
<router-link :to="{ path: '/search', query: { category: cat.link } }">
<div>
<img :src="cat.headerImage" />
</div>
<span>{{ cat.name }}</span>
</router-link>
</li>
<li>
<router-link :to="{ path: '/search', query: { category: item.link } }">
<div>
<i class="isax isax-category" />
</div>
<span>{{ $t('allProducts') }}</span>
</router-link>
</li>
</ul>
</AccordionTab>
</template>
</Accordion>
</div>
</main>
</NuxtLayout>
</template>
<script setup>
const config = reactive({
mobile: { header: { search: true, menu: true }, navigation: true },
})
const { device } = inject('service')
if (device.value == 'desktop') navigateTo('/')
const categories = useCategoriesStore()
const items = categories.items
const sub = ref(0)
</script>
<style lang="scss">
.categories {
@apply max-w-full w-full h-screen flex lg:hidden;
&>div:nth-of-type(1) {
@apply overflow-y-auto pb-14 w-max shrink-0;
&::-webkit-scrollbar {
@apply hidden;
}
&>ul {
@apply flex flex-col bg-[#FAFAFA] min-h-full;
li {
@apply w-[5.7rem] h-[5.3rem] border-l border-b border-[#F1F2F4] text-[#333333] cursor-pointer;
div {
@apply w-full h-full flex flex-col gap-[0.3rem] items-center justify-center overflow-hidden;
i {
@apply text-xl;
}
span {
@apply text-center text-[0.6em] font-vazir;
}
}
&[active='true'] {
@apply border-l-0 bg-white text-primary-600;
}
}
}
}
&>div:nth-of-type(2) {
@apply flex flex-col px-6 grow;
&>a>button {
@apply w-max h-max -mr-4 #{!important};
.p-button-label {
@apply text-right text-[0.6em] font-medium font-vazir;
}
.p-button-icon {
@apply text-[0.5em] #{!important};
}
}
.p-accordion {
@apply w-full;
.p-accordion-tab {
@apply border-t border-[#F1F2F4] first:border-t-0;
.p-accordion-header {
@apply w-full h-11 flex items-center;
.p-accordion-header-link {
@apply bg-transparent border-none w-full flex justify-between px-0;
.p-accordion-header-text {
@apply text-right text-[#333333] text-xs font-medium font-vazir;
}
.p-accordion-toggle-icon {
@apply text-[#333333];
}
}
}
.p-accordion-content {
@apply pb-[1.1rem] pt-3 rtl border-none px-0;
ul {
@apply grid grid-cols-3 gap-6 w-max;
li {
@apply w-max h-max;
a {
@apply w-[3.8rem] h-[5.5rem] flex flex-col items-center gap-3;
div {
@apply rounded-full w-[3.8rem] h-[3.8rem] bg-[#F0F0F1] flex;
img {
@apply m-auto w-11;
}
i {
@apply m-auto text-2xl;
}
}
span {
@apply text-[#333333] text-[0.6em] font-vazir;
}
}
}
}
}
&:not(:has(li)) .p-accordion-toggle-icon {
@apply invisible;
}
}
}
}
}
</style>
+53
View File
@@ -0,0 +1,53 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="cart">
<CartTabs />
<section>
<div>
<ul v-if="cart.items.length > 0">
<li v-for="(item, i) in cart.items" :key="i">
<CartItem :data="item.product" />
</li>
</ul>
<CartTotalAside />
</div>
</section>
<CartTotalFixed v-if="device == 'mobile'" />
<CartBuyersBought />
</main>
</NuxtLayout>
</template>
<script setup>
const config = reactive({ mobile: { header: false } })
const { device } = inject('service')
const cart = useCartStore()
cart.get()
provide('data', computed(() => cart.items))
</script>
<style lang="scss">
.cart {
@apply w-full flex flex-col gap-[2.6rem] pb-32 lg:gap-6 lg:pt-10;
&>section:not([class]) {
@apply w-full min-h-96 flex lg:mb-[6.4rem];
&>div {
@apply container flex flex-col items-center lg:items-start lg:flex-row gap-6;
&>ul {
@apply max-w-[67.6rem] flex flex-col border-b lg:rounded-[0.6rem] lg:border border-[#E5E5E5] grow;
&>li {
@apply p-6 lg:p-10 border-b last:border-b-0 border-[#E5E5E5];
}
}
}
}
}
</style>
+13
View File
@@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script setup>
</script>
<style lang="scss">
</style>
+11
View File
@@ -0,0 +1,11 @@
<template>
<div>
</div>
</template>
<script setup>
</script>
<style lang="scss"></style>
+11
View File
@@ -0,0 +1,11 @@
<template>
<div>
</div>
</template>
<script setup>
</script>
<style lang="scss"></style>
+34
View File
@@ -0,0 +1,34 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="compare">
<CompareProductList />
<CompareSpecList />
</main>
</NuxtLayout>
</template>
<script setup>
import init from '../../initialize/compare'
const { options } = init()
const config = reactive({
mobile: { header: { back: true, options } },
})
const { device } = inject('service')
const { meta, params } = useRoute()
const { status, data, error } = await useFetch(`/api/products/comparison/${params.ids}`)
if (status.value == 'success') {
provide('data', data.value)
}
else throw createError(error.value)
</script>
<style lang="scss">
.compare {
@apply flex flex-col;
}
</style>
+40
View File
@@ -0,0 +1,40 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="home">
<HomeTopSlider />
<HomeMainCategories />
<HomeBanners />
<HomeAmazingOffers />
<HomeReviews />
<HomeEstProducts />
<HomePopularCategories />
<HomeLatestPosts />
<HomeProducers :title="$t('producers')" />
<HomeAsanService />
</main>
</NuxtLayout>
</template>
<script setup>
import init from '../initialize/home'
provide('init', init)
const { device } = inject('service')
const config = reactive({
mobile: { header: { search: true, menu: true } },
})
const { status, data, error } = await useFetch('/api/pages/landing')
if (status.value == 'success') {
useSeoMeta(data.value.meta || {})
provide('data', data.value)
}
else throw createError(error.value)
</script>
<style lang="scss">
.home {
@apply flex flex-col max-lg:pb-24;
}
</style>
+93
View File
@@ -0,0 +1,93 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="about-us">
<section>
<h1>{{ $t("aboutUs") }}</h1>
<ul>
<li v-for="({ title, content }, i) in items" :key="i">
<span />
<div>
<h2>{{ title }}</h2>
<div v-html="content" />
</div>
</li>
</ul>
</section>
</main>
</NuxtLayout>
</template>
<script setup>
definePageMeta({ path: '/about-us' })
const config = reactive({
mobile: {
header: { search: true, menu: true },
navigation: false
},
})
const { device } = inject('service')
const items = ref([])
const { status, data, error } = await useFetch('/api/abouts')
if (status.value == 'success')
items.value = data.value
else throw createError(error.value)
</script>
<style lang="scss">
.about-us {
@apply w-full flex pt-8 pb-5 lg:pt-[6.5rem] lg:pb-[9.3rem];
section {
@apply w-full flex flex-col gap-6 lg:gap-[4.1rem];
h1 {
@apply mx-auto text-zinc-800 font-black font-vazir text-lg lg:text-3xl;
}
ul {
@apply flex flex-col;
li {
&:nth-child(odd) {
@apply bg-slate-100;
}
&:nth-child(even) {
@apply bg-zinc-100;
}
@apply w-full flex overflow-hidden;
@apply h-[14.7rem] gap-[1.1rem] p-6;
@apply lg:h-[17.8rem] lg:gap-[5.5rem] lg:px-[5.3rem] lg:py-[4.4rem] lg:items-center;
span {
@apply max-lg:hidden w-36 h-36 bg-white shrink-0;
}
&>div {
@apply flex flex-col font-vazir gap-[1.1rem] xl:gap-6;
h2 {
@apply w-max text-primary-600 font-black border-b-2 border-current pb-2;
@apply text-lg lg:text-3xl lg:border-b-4 lg:pb-2.5;
}
div {
p {
@apply text-zinc-800 font-normal font-vazir;
@apply w-auto max-w-[53.5rem] text-sm lg:text-xl leading-6;
}
}
}
}
}
}
}
</style>
+239
View File
@@ -0,0 +1,239 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="contact-us">
<div>
<h3>{{ $t("usAccompany") }}</h3>
<span />
<ul>
<li v-for="(social, i) in socials" :key="i">
<router-link :to="social.url">
<img :src="`/icons/${social.icon}.svg`" />
</router-link>
</li>
</ul>
</div>
<section>
<form @input="clearError()">
<h2>{{ $t('contactUs') }}</h2>
<p>{{ $t('enterContantAndMessage') }}</p>
<ul>
<li v-for="(item, key) in fields" :key="key" :error="item.error">
<component :is="item.is" :id="key" v-model="form[key]" v-bind="item.props" />
<label :for="key">{{ item.label }}</label>
</li>
</ul>
<Button :label="$t('sendMessage')" :loading="loading" @click="send()" />
</form>
<div>
<ul>
<li v-for="(way, i) in ways" :key="i">
<i class="isax" :class="way.icon"></i>
<label>{{ $t(way.label) }}:</label>
<span>{{ way.value }}</span>
</li>
</ul>
</div>
<div>
<div>
<LMap ref="leaflet" v-bind="map.props">
<LTileLayer v-bind="map.layer" />
</LMap>
</div>
</div>
</section>
</main>
</NuxtLayout>
</template>
<script setup>
import init from '../../initialize/contact-us.js'
const { socials, fields, ways, map } = init()
definePageMeta({ path: '/contact-us' })
const config = reactive({
mobile: {
header: { search: true, menu: true },
navigation: false
},
})
const { t, toast, device } = inject('service')
const form = ref({})
const loading = ref(false)
const send = async () => {
clearError()
loading.value = true
const { status, error } = await useFetch('/api/contactus', { method: 'post', body: form.value })
loading.value = false
if (status.value == 'success') {
toast.add({
life: 3000, severity: 'success', summary: t('success'), detail: t('sendSuccessfully')
})
form.value = {}
} else if (error.value.statusCode == 422) {
const data = error.value.data
delete data.phoneNumber
Object.entries(data).forEach(([key, value]) => {
fields[key].error = value
fields[key].props.class = '!border-red-500'
})
} else toast.add({
life: 3000, severity: 'error', summary: t('error'), detail: error.value.message
})
}
const clearError = () => {
Object.keys(fields).forEach((key) => {
delete fields[key].error
delete fields[key].props.class
})
}
</script>
<style lang="scss">
.contact-us {
@apply w-full flex px-6 lg:px-20 my-8 lg:mt-16 lg:pb-[9.4rem] relative;
&>div {
@apply hidden lg:w-[1.6rem] lg:h-[20rem] lg:flex flex-col items-center gap-4;
&>h3 {
@screen sm {
writing-mode: vertical-rl;
}
@apply whitespace-nowrap text-zinc-400 font-vazir sm:-scale-100;
}
&>span {
@apply h-px w-[4.4rem] sm:w-px sm:h-[4.4rem] bg-[#E5E5E5];
}
&>ul {
@apply flex sm:flex-col gap-4 mt-auto;
li {
@apply opacity-60;
img{
@apply brightness-[0.4];
}
}
}
}
&>section {
@apply container flex flex-col flex-wrap lg:flex-row justify-center items-center md:justify-start;
@apply xl:gap-x-[8.5rem] md:h-[37rem] lg:h-auto lg:justify-center;
&>form {
@apply w-[21.4rem] lg:w-[35.3rem] flex flex-col;
h2 {
@apply text-[#333333] text-lg lg:text-3xl font-black font-vazir flex flex-col gap-2.5 lg:gap-5;
&::after {
@apply content-['_'] w-10 h-0.5 lg:w-20 lg:h-1 bg-primary-600 rounded-xl;
}
}
p {
@apply text-[#7F7F7F] text-sm lg:text-xl font-vazir mt-5 lg:mt-4 mb-auto;
}
ul {
@apply flex flex-col gap-6 mt-10 lg:mt-12;
li {
@apply w-full relative flex items-center;
label {
@apply w-max absolute right-3 top-3.5 px-1 bg-white transition-['top'];
@apply text-[#999999] text-base font-medium font-vazir cursor-text;
}
input {
@apply h-14;
}
textarea {
@apply h-[7.6rem] lg:h-[12.6rem];
}
input,
textarea {
@apply rtl w-full rounded-[0.6rem] border shadow-none;
@apply border-[#F2F2F2] text-zinc-800 font-vazir;
}
&:has(input:focus),
&:has(textarea:focus),
&:has(input:not(:placeholder-shown)),
&:has(textarea:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal font-vazir;
}
}
&::after {
content: attr(error);
@apply text-red-500 text-sm absolute -bottom-5 left-0 font-vazir;
}
}
}
button {
@apply w-full h-12 lg:h-14 mt-5 #{!important};
.p-button-label {
@apply text-white text-sm font-normal lg:text-xl lg:font-medium font-iran-sans;
}
}
}
&>div:first-of-type {
@apply flex md:order-last mt-14 lg:mt-[6.3rem];
&>ul {
@apply flex flex-col sm:flex-wrap sm:flex-row;
@apply w-full h-auto gap-4;
@apply max-w-[21.4rem] lg:max-w-[80.7rem] lg:h-[5.4rem] lg:gap-x-20 lg:gap-y-8;
li {
i {
@apply ml-3 lg:ml-4 text-[1.1em] lg:text-2xl text-[#7F7F7F] align-middle;
}
label {
@apply ml-2 text-[#7F7F7F] text-sm lg:text-[1.1em] font-medium font-vazir whitespace-nowrap;
}
span {
@apply text-[#333333] text-sm lg:text-[1.1em] font-medium font-vazir;
}
}
}
}
&>div:last-of-type {
@apply flex relative w-[21.4rem] h-[17.3rem] lg:w-[37rem] lg:h-[37rem] max-md:mt-5 lg:mt-20 2xl:mt-0;
&::before {
@apply lg:content-['_'] absolute bg-primary-600 rounded-2xl top-0 left-0;
@apply w-[14.5rem] h-[29rem];
}
&>div {
@apply rounded-[0.6rem] bg-yellow-300 overflow-hidden my-auto;
@apply w-full h-full lg:w-[33.8rem] lg:h-[33.8rem] z-0;
}
}
}
}
</style>
+13
View File
@@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script setup>
</script>
<style lang="scss">
</style>
+13
View File
@@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script setup>
</script>
<style lang="scss">
</style>
+13
View File
@@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script setup>
</script>
<style lang="scss">
</style>
+93
View File
@@ -0,0 +1,93 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="panel-comments">
<ul>
<li v-for="(comment, i) in comments" :key="i">
<PanelCommentItem :data="comment" />
</li>
</ul>
<AppPagination :pages="pages" />
</main>
</NuxtLayout>
</template>
<script setup>
import init from '../../initialize/panel'
provide('init', init())
const { device, t } = inject('service')
const config = reactive({
desktop: { toolbar: true },
mobile: { header: { back: true, title: t('myComments') } },
})
const comments = ref([])
const pages = ref(1)
const { meta, query } = useRoute()
const { page = 1 } = query
const { status, data, error } = await useFetch(`/api/comments/${page}`, {
headers: {
Authorization: useCookie('token')
}
})
if (status.value == 'success') {
comments.value = data.value
pages.value = data.value.pages
} else throw createError(error.value)
</script>
<style lang="scss">
.panel-comments {
@apply w-full container max-lg:px-6 pb-20 pt-[1.9rem] lg:py-[6.3rem];
ul {
@apply flex flex-col justify-center mx-auto gap-6 lg:gap-[2.6rem];
li {
@apply border-b pb-6 lg:pb-10 border-[#B9B9B9] last:border-none;
}
}
}
body:has(.panel-comments) {
.p-dialog-mask {
@apply z-10 #{!important};
.p-dialog {
@apply m-0 max-lg:w-full #{!important};
}
.p-dialog-content {
@apply lg:container w-screen lg:w-max flex lg:px-4 #{!important};
}
}
.p-menu {
@apply w-[10.8rem];
}
.p-menuitem {
@apply text-base;
&:first-child span {
@apply text-[#4C4C4C] #{!important};
}
&:last-child span {
@apply text-[#AD3434] #{!important};
}
}
.p-menuitem-content a {
@apply rtl px-5 #{!important};
.p-menuitem-icon {
@apply mr-0 ml-3 #{!important};
}
}
}
</style>
+257
View File
@@ -0,0 +1,257 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="user-profile-edit">
<div>
<h1>{{ $t("userProfileEdit") }}</h1>
</div>
<div>
<div>
<img :src="image" />
<input ref="input" type="file" hidden @change="chooseImage" />
<Button v-bind="btns.upload.props" @click="showImages()" />
</div>
<div>
<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>
<div>
<Button v-bind="btns.form.cancel.props" @click="$router.go(-1)" />
<Button v-bind="btns.form.save.props" :loading="loading" @click="save()" />
</div>
</div>
</div>
</main>
</NuxtLayout>
</template>
<script setup>
import iranCities from "../../initialize/iranCities"
import init from "../../initialize/userProfileEdit";
const { device, toast, t } = inject('service')
const config = reactive({
mobile: { header: { title: t('edit') } }
})
const { btns, fields } = init();
const source = ref();
const image = ref();
const input = ref();
const form = reactive({});
const errors = ref({});
const loading = ref(false);
const router = useRouter();
const { status, data, error } = await useFetch('/api/users/current', {
headers: {
Authorization: useCookie('token')
},
})
const provinces = reactive(iranCities)
fields.state.props.options = provinces
watch(() => form.state, (v) => {
provinces.forEach(({ name, cities }) => {
if (name == v) {
fields.city.props.options = reactive(cities)
}
});
}, { deep: true })
fields.city.props.disabled = computed(() => fields.city.props.options.length < 1)
if (status.value == 'success') {
Object.keys(fields).forEach(key => {
form[key] = data.value[key]
});
image.value = data.value.profilePhoto ?? '/images/empty-profile.svg'
}
else throw createError(error.value)
const showImages = () => input.value.click()
const chooseImage = (e) => {
const binaryData = [];
binaryData.push(e.target.files[0]);
image.value = URL.createObjectURL(new Blob(binaryData, { type: "image" }))
source.value = e.target.files[0];
}
const save = async () => {
errors.value = {}
loading.value = true
if (source.value) {
const formData = new FormData()
formData.append('images', source.value)
const { status, data, error } = await useFetch('/api/images/upload', {
headers: { Authorization: useCookie('token') },
params: { type: 3 },
body: formData,
method: 'post'
})
if (status.value == "success") {
form.profilePhoto = data.value.urls[0]
}
}
const { status, data, error } = await useFetch('/api/users/', {
headers: { Authorization: useCookie('token') }, body: Object.assign({}, form), method: 'patch'
})
if (status.value == "success") {
router.go(-1)
toast.add({
life: 5000, severity: 'success', summary: t('success'), detail: t('updateSuccessfull')
})
} 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 || error.value.message
})
}
loading.value = false
}
</script>
<style lang="scss">
.user-profile-edit {
@apply w-full flex flex-col gap-[5.5rem] mb-[8.8rem];
&>div:nth-of-type(1) {
@apply h-[5.9rem] bg-[#F2F2F2] flex items-center justify-center;
h1 {
@apply text-[#333333] text-2xl font-bold font-iran-sans;
}
}
&>div:nth-of-type(2) {
@apply flex gap-20 justify-center self-center flex-col md:flex-row w-full sm:w-auto px-4;
&>div:nth-of-type(1) {
@apply flex flex-col gap-6 self-center md:self-start;
img {
@apply w-[12.4rem] h-[12.4rem] rounded-[0.6rem];
}
button {
@apply w-[12.4rem] h-12 p-0 justify-center #{!important};
.p-button-label {
@apply text-sm font-medium font-iran-sans grow-0;
}
.p-button-icon {
@apply text-xl;
}
}
}
&>div:nth-of-type(2) {
@apply overflow-y-hidden h-max -mt-4 flex flex-col gap-14;
&>div:nth-of-type(1) {
@apply md:overflow-y-auto md:max-h-[40rem] w-full;
&::-webkit-scrollbar-track {
@apply bg-transparent my-4;
}
ul {
@apply flex flex-col gap-6 w-full max-w-full md:pl-7 md:py-4 my-0 pr-px;
li {
@apply w-full sm:w-[27.1rem] h-14 relative col-span-2;
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,
textarea {
@apply rtl w-full rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
input[type] {
@apply ltr;
}
&:has(input:focus),
&:has(textarea:focus),
&:has(.p-inputwrapper-filled),
&:has(input:not(:placeholder-shown)),
&:has(textarea:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal font-vazir;
}
}
.p-inputnumber,
.p-dropdown,
.p-calendar {
@apply w-full;
}
.p-dropdown .p-dropdown-label {
@apply font-vazir leading-7;
}
.p-calendar {
@apply flex items-center relative z-0 gap-4 font-['iconsax'] #{!important};
&::before {
@apply text-xl absolute left-3 z-[1] mb-1;
}
input {
@apply pl-10 #{!important};
}
}
.p-inputnumber input {
@apply ltr;
}
small {
@apply text-red-500 font-vazir float-left;
}
&:has(small) * {
@apply border-red-500 #{!important};
}
}
}
}
&>div:nth-of-type(2) {
@apply flex flex-wrap gap-3;
&>button {
@apply w-full sm:w-[13.2rem] h-12;
.p-button-label {
@apply text-sm font-medium font-iran-sans;
}
}
}
}
}
}
</style>
+121
View File
@@ -0,0 +1,121 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="panel-factors">
<div>
<form>
<i class="isax isax-search-normal-1" />
<input v-model="filters.q" :placeholder="$t('search')" />
<Button icon="isax isax-setting-4" severity="secondary" text rounded @click="showFilters" />
</form>
<aside v-if="lastFactors?.length > 0">
<h2>{{ $t("recentFactors") }}</h2>
<ul>
<li v-for="(factor, i) in lastFactors" :key="i">
<PanelRecentFactorItem :data="factor" />
</li>
</ul>
</aside>
</div>
<div>
<PanelSalesFactor :data="factor" />
</div>
</main>
</NuxtLayout>
</template>
<script setup>
const { device, dialog, popup, t } = inject('service');
const config = reactive({
desktop: { toolbar: true }, mobile: { header: { back: true, title: t('factors') } },
})
const { meta, query } = useRoute()
const { page = 1 } = query
const lastFactors = ref([])
const factor = ref([])
const filters = ref({});
const route = useRoute()
const id = route.params.id || 1
const { status, data, error } = await useFetch(`/api/orders/id/${id}`, {
headers: {
Authorization: useCookie('token')
}
})
if (status.value == 'success') {
lastFactors.value = data.value.lastFactors
factor.value = data.value.factor
} else throw createError(error.value)
const component = resolveComponent('PanelFactorFilters')
const showFilters = (e) => {
if (device.value == 'desktop') {
popup.value.is = component
popup.value.toggle(e)
} else if (device.value == 'mobile')
dialog.show(component)
}
</script>
<style lang="scss">
.panel-factors {
@apply w-full lg:container relative;
@apply pt-8 pb-20 lg:pt-14 lg:pb-[7.5rem] lg:min-h-screen;
&>div:nth-of-type(1) {
@apply flex flex-col md:flex-row justify-between gap-6 max-lg:px-6;
&>form {
@apply w-full md:max-w-[38rem] h-[2.9rem] bg-[#FAFAFA] rounded-[0.6rem] flex items-center pr-4;
i {
@apply text-lg;
}
input {
@apply h-full px-2 bg-transparent grow outline-none;
&::placeholder {
@apply text-neutral-400 text-lg font-vazir;
}
}
&>button {
@apply h-full;
.p-button-icon {
@apply text-2xl;
}
}
}
&>aside {
@apply max-w-full w-max flex flex-col gap-[1.1rem] lg:gap-12;
h2 {
@apply text-[#333333] text-base lg:text-xl font-bold font-vazir max-lg:pr-2.5;
}
ul {
@apply flex justify-around flex-wrap md:flex-col gap-3 p-3 bg-[#FAFAFA] rounded-[0.6rem] overflow-hidden;
}
}
}
&>div:nth-of-type(2) {
@apply max-w-full overflow-x-auto left-[20.8rem] right-6 lg:left-[19.3rem] lg:right-0 md:absolute top-full md:top-[5.3rem] lg:pb-4;
@apply p-1 mt-[2.6rem];
&::-webkit-scrollbar {
@apply max-lg:hidden;
}
}
}
</style>
+80
View File
@@ -0,0 +1,80 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="panel-favorites">
<ul>
<li v-for="(product, i) in items" :key="i">
<ProductCard :data="product" type="main" />
</li>
</ul>
<AppPagination :pages="pages" />
</main>
</NuxtLayout>
</template>
<script setup>
const { device, t } = inject('service')
const config = reactive({
desktop: { toolbar: true }, mobile: { header: { back: true, title: t('favorites') } },
})
const items = ref([])
const pages = ref(1)
const { meta, query } = useRoute()
const { page = 1 } = query
const { status, data, error } = await useFetch(`/api/users/current/fave/${page}`, {
headers: {
Authorization: useCookie('token')
}
})
if (status.value == 'success') {
items.value = data.value.favoriteProducts
pages.value = data.value.pages
} else throw createError(error.value)
</script>
<style lang="scss">
.panel-favorites {
@apply flex flex-col gap-3 lg:gap-6 justify-center lg:justify-start mx-auto lg:container;
@apply pt-8 pb-20 lg:pt-14 lg:pb-[7.5rem] lg:min-h-[60vh];
&>ul {
@apply w-full flex flex-wrap gap-3 lg:gap-6 justify-center lg:justify-start mx-auto lg:container;
@media (max-width: 1024px) {
.product-card {
@apply w-[21.4rem] h-[9.4rem] flex-row shadow-none border-0 rounded-none gap-4 p-0;
@apply mt-[1.1rem] border-b border-[#E5E5E5];
&>img {
@apply w-[7.4rem] h-[7.4rem] object-contain object-top self-start;
}
&>div {
@apply pb-6;
h2 {
@apply mt-0 text-[0.7em];
}
h1 {
@apply text-xs;
}
&>button {
@apply left-0 bottom-3;
.p-button-icon {
@apply text-xl;
}
}
}
}
}
}
}
</style>
+52
View File
@@ -0,0 +1,52 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="panel-information">
<PanelInformation />
<PanelBecomeSeller v-if="type == 'BUYER'" />
<PanelNationalCard v-if="type == 'RETAILER'" />
</main>
</NuxtLayout>
</template>
<script setup>
const { device, t } = inject('service')
const config = reactive({
mobile: {
header: {
title: t('userInfo'),
back: true,
options: [
{
props: {
icon: "isax isax-edit-2", severity: "secondary", text: true, rounded: true,
click: () => navigateTo('/panel/edit')
}
}
]
}
},
})
const { meta } = useRoute()
if (device.value == 'desktop') navigateTo('/panel/profile')
const { status, data, error } = await useFetch('/api/users/current', {
headers: {
Authorization: useCookie('token')
}
})
const type = ref('')
if (status.value == 'success') {
type.value = data.value.accountType
provide('data', data.value)
}
else throw createError(error.value)
</script>
<style lang="scss">
.panel-information {
@apply flex flex-col px-6 pt-[1.9rem] pb-20 gap-[2.6rem];
}
</style>
+143
View File
@@ -0,0 +1,143 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="panel-moeen-factors">
<table>
<caption>
<ul>
<li v-for="(value, key) in info" :key="key">
<label v-text="$t(key) + ':'" />
<span v-text="value" />
</li>
</ul>
</caption>
<thead>
<tr>
<th v-for="(column, i) in columns" :key="i" v-text="$t(column)" />
</tr>
</thead>
<tbody>
<tr></tr>
<tr v-for="(row, i) in rows" :key="i">
<td v-text="i + 1" />
<td v-for="(value, key) in row" :key="key" v-text="value" />
</tr>
</tbody>
</table>
</main>
</NuxtLayout>
</template>
<script setup>
const { device, t } = inject('service')
const config = reactive({
desktop: { toolbar: true }, mobile: { header: { back: true, title: t('moeenFactors') } },
})
const { meta, query } = useRoute()
const { page = 1 } = query
const info = ref([])
const rows = ref([])
const columns = ref([])
const { status, data, error } = await useFetch(`/api/moeens/user`, {
headers: {
Authorization: useCookie('token')
}
})
if (status.value == 'success') {
// info.value = data.value.info
// rows.value = data.value.items
// columns.value = ["row", ...Object.keys(rows.value[0]), ""]
} else throw createError(error.value)
const show = () => { }
</script>
<style lang="scss">
.panel-moeen-factors {
@apply container w-full flex flex-col items-center max-lg:px-6 max-lg:gap-4;
@apply pt-8 pb-20 lg:pt-14 lg:pb-[7.5rem] overflow-x-auto;
&::-webkit-scrollbar {
@apply hidden;
}
table {
@apply w-[100.3rem] border border-[#E5E5E5];
caption {
@apply h-[3.6rem] border-x border-t px-6 border-[#E5E5E5];
ul {
@apply h-full flex items-center gap-4 text-xs lg:text-base;
li {
@apply flex items-center gap-2 font-vazir whitespace-nowrap;
label {
@apply text-[#333333] font-bold;
}
span {
@apply text-[#7F7F7F] font-medium;
}
&:nth-child(1) {
@apply ml-[8.6rem];
}
&:nth-child(3) {
@apply ml-[8.8rem] mr-auto;
}
}
}
}
thead {
tr {
@apply h-[2.8rem] border border-[#E5E5E5] bg-[#F2F2F2];
th {
@apply border-l border-[#E5E5E5] px-2.5 text-xs lg:text-base;
@apply text-center text-[#333333] font-medium font-vazir;
}
}
}
tbody {
@apply h-max;
tr {
@apply border-b h-[2.8rem] w-full;
td {
@apply border-l border-[#E5E5E5] px-2.5 text-[0.7em] lg:text-sm;
@apply text-center text-zinc-800 font-medium font-vazir;
&:nth-child(6) {
@apply w-[17.2rem] text-right;
}
&:nth-child(12) {
@apply w-[3.3rem];
button {
.p-button-icon {
@apply text-2xl text-[#B2B2B2];
}
}
}
}
&:first-child {
direction: ltr;
@apply ltr;
}
}
}
}
}
</style>
+48
View File
@@ -0,0 +1,48 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="panel-notifications">
<ul>
<li v-for="(item, i) in items" :key="i">
<PanelNotificationItem :data="item" />
</li>
</ul>
<AppPagination :pages="pages" />
</main>
</NuxtLayout>
</template>
<script setup>
const { device, t } = inject('service')
const config = reactive({
desktop: { toolbar: true }, mobile: { header: { back: true, title: t('notifications') } },
})
const items = ref([])
const pages = ref(1)
const { meta, query } = useRoute()
const { page = 1 } = query
const { status, data, error } = await useFetch(`/api/users/current/notif/${page}`, {
headers: {
Authorization: useCookie('token')
}
})
if (status.value == 'success') {
items.value = data.value.notifications
pages.value = data.value.pages
} else throw createError(error.value)
</script>
<style lang="scss">
.panel-notifications {
@apply container w-full flex flex-col items-center max-lg:px-6 max-lg:gap-4;
@apply pt-8 pb-20 lg:pt-14 lg:pb-[7.5rem] max-lg:overflow-hidden;
&>ul {
@apply w-full flex flex-col gap-4;
}
}
</style>
+52
View File
@@ -0,0 +1,52 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="panel-orders">
<ul>
<li v-for="(order, i) in items" :key="i">
<PanelOrderItem :data="order" />
</li>
</ul>
<AppPagination :pages="pages" />
</main>
</NuxtLayout>
</template>
<script setup>
const { device, t } = inject('service')
const config = reactive({
desktop: { toolbar: true }, mobile: { header: { back: true, title: t('orders') } },
})
const { meta, query } = useRoute()
const { page = 1 } = query
const { status, data, error } = await useFetch(`/api/orders/user/${page}`, {
headers: {
Authorization: useCookie('token')
}
})
const items = ref([])
const pages = ref(1)
if (status.value == 'success') {
items.value = data.value.items
pages.value = data.value.pages
} else throw createError(error.value)
</script>
<style lang="scss">
.panel-orders {
@apply w-full lg:container flex flex-col gap-y-10 lg:gap-[2.6rem];
@apply px-6 sm:px-4 lg:px-0 pt-8 pb-20 lg:pt-14 lg:pb-[14.9rem];
&>ul:first-child {
@apply flex flex-col gap-x-6 gap-y-10 lg:gap-[2.6rem];
}
.pagination {
@apply self-center mt-auto;
}
}
</style>
+55
View File
@@ -0,0 +1,55 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="panel-profile">
<PanelProfileCard />
<template v-if="device == 'desktop'">
<PanelInformation />
<PanelBecomeSeller v-if="type == 'BUYER'" />
<PanelNationalCard v-else />
</template>
<template v-else-if="device == 'mobile'">
<hr />
<PanelPorfileMenu />
</template>
</main>
</NuxtLayout>
</template>
<script setup>
import init from '../../initialize/panel'
const { meta } = useRoute()
meta.init = init()
provide('init', meta.init)
const config = reactive({
desktop: { toolbar: true }, mobile: { header: false },
})
const type = ref('')
const { device } = inject('service')
const { status, data, error } = await useFetch('/api/users/current', {
headers: {
Authorization: useCookie('token')
}
})
if (status.value == 'success') {
type.value = data.value.accountType
meta.confirmToSales = data.value.confirmToSales
provide('data', data.value)
}
else throw createError(error.value)
</script>
<style lang="scss">
.panel-profile {
@apply container flex flex-wrap justify-evenly items-center gap-[1.9rem];
@apply lg:gap-5 px-6 sm:px-0 pt-8 pb-20 lg:pt-14 lg:pb-[19.3rem];
hr {
@apply bg-[#E5E5E5] w-full;
}
}
</style>
+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>
+125
View File
@@ -0,0 +1,125 @@
<template>
<NuxtLayout v-if="config" :name="device" :config="config">
<main class="product">
<AppBreadcrumbs :items="breadcrumbs" />
<ProductWrapper />
<template v-if="device == 'desktop'">
<ProductSpecification />
<ProductDescription />
<ProductCommentWrapper />
</template>
<TabView v-else-if="device == 'mobile'">
<TabPanel :header="t('specifications')">
<ProductSpecification />
</TabPanel>
<TabPanel :header="t('description')">
<ProductDescription />
</TabPanel>
<TabPanel :header="t('buyersComments')">
<ProductCommentTabView />
</TabPanel>
</TabView>
<ProductSimilars />
</main>
</NuxtLayout>
</template>
<script setup>
import init from '../../initialize/product'
const { meta, params, path } = useRoute()
const config = ref()
const { device, t } = inject('service')
const breadcrumbs = reactive([
{ name: t('asanMarket'), link: '/' },
{ name: t('products'), link: '/search' }
])
const { status, data, error } = await useFetch(`/api/products/uid/${params.uid}`)
if (status.value == 'success') {
const product = data.value
breadcrumbs.push(...product.category.map((cat) => {
cat.link = '/search?category=' + cat.link
return cat
}))
breadcrumbs.push({ name: product.title, link: path })
const { options, ...others } = init(data.value)
meta.init = others
provide('data', data.value)
provide('init', others)
config.value = {
mobile: { header: { options }, navigation: false },
}
}
else throw createError(error.value)
</script>
<style lang="scss">
.product {
@apply w-full flex flex-col pb-32;
.breadcrumbs {
@apply px-6 pt-6 lg:pt-14 justify-start;
}
.wrapper {
@apply mt-12 lg:px-6;
}
.specification {
@apply mt-[1.1rem] lg:mt-[4.9rem];
}
.description {
@apply mt-9 lg:mt-[6rem];
}
.comments {
@apply mt-[1.1rem] lg:mt-[10rem];
}
.similars {
@apply mt-[2.6rem] lg:mt-[10rem];
}
.p-tabview {
@apply mt-12 rtl;
.p-tabview-nav-content {
@apply px-6;
.p-tabview-nav {
@apply border-[#E5E5E5];
.p-tabview-nav-link {
@apply h-[2.3rem] p-2.5 border-[#E5E5E5];
.p-tabview-title {
@apply text-zinc-800 text-[0.7em] font-medium font-vazir;
}
}
.p-tabview-header.p-highlight .p-tabview-nav-link {
@apply border-[#47B556];
.p-tabview-title {
@apply text-[#47B556];
}
}
}
}
.p-tabview-panels {
@apply p-0;
}
}
}
</style>
+85
View File
@@ -0,0 +1,85 @@
<template>
<NuxtLayout :name="device" :config="config">
<main class="search-product">
<SearchBrand v-if="data.brand" />
<AppBreadcrumbs v-else-if="device == 'desktop'" :items="breadcrumbs" />
<SearchWrapper />
<HomeProducers v-if="data.brand" :title="$t('otherProducers')" />
</main>
</NuxtLayout>
</template>
<script setup>
import init from '../../initialize/search'
const config = reactive({
mobile: { header: { search: true, menu: true } },
})
const { device, t } = inject('service')
const breadcrumbs = reactive([
{ name: t('asanMarket'), link: '/' },
{ name: t('products'), link: '/search' }
])
const route = useRoute()
const { page = 1, sort = 'mostVisited', ...filters } = route.query
const { status, data, error } = await useFetch(`/api/products/search/${page}`, {
params: { sort, filters }
})
if (status.value == 'success') {
if (filters.category) {
const temp = []
const func = (items) => {
let finded = false
items.forEach((item) => {
if (!finded) {
if (item.link == filters.category) {
temp.unshift(Object.assign({}, item))
finded = true
} else {
finded = func(item.sub ?? [])
if (finded) {
temp.unshift(Object.assign({}, item))
}
}
}
})
return finded
}
const categories = useCategoriesStore()
func(categories.items)
const items = temp.map((item) => {
item.link = '/search?category=' + item.link
return item
})
breadcrumbs.push(...items)
}
if (filters.q)
breadcrumbs.push({ name: filters.q, link: route.path })
provide('data', data.value)
route.meta.init = init()
provide('init', route.meta.init)
} else throw createError(error.value)
</script>
<style lang="scss">
.search-product {
@apply flex flex-col mb-[6.8rem] bg-white;
.breadcrumbs {
@apply px-3 justify-start mt-8 mb-2.5 max-lg:hidden;
}
}
</style>