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
+50
View File
@@ -0,0 +1,50 @@
<template>
<section class="breadcrumbs">
<div>
<ul>
<li v-for="(item, i) in items" :key="i">
<router-link :to="item.link">
{{ item.name }}
</router-link>
</li>
</ul>
</div>
</section>
</template>
<script setup>
defineProps({ items: { type: Array, default: [] } });
</script>
<style lang="scss">
.breadcrumbs {
@apply w-full flex max-lg:px-6;
div {
@apply overflow-x-auto h-max;
&::-webkit-scrollbar{
@apply hidden;
}
ul {
@apply flex items-center gap-3.5 lg:gap-6 w-max;
li {
@apply relative text-[0.7em] lg:text-[1.1em];
a {
@apply text-[#999999] font-bold font-vazir;
}
&:not(:last-child)::after {
@apply content-['/'] absolute -left-2 lg:-left-3.5;
}
&:last-child a {
@apply text-[#666666];
}
}
}
}
}
</style>
File diff suppressed because it is too large Load Diff
+96
View File
@@ -0,0 +1,96 @@
<template>
<ul v-if="pages > 1" class="pagination">
<li v-for="i in [-1, 1]" :key="i">
<router-link
:to="{ query: { ...$route.query, page: page + i } }"
:class="{ 'pointer-events-none': page + i < 1 || page + i > pages }"
>
<Button :icon="getIcon(i)" link />
</router-link>
</li>
<li v-for="(item, i) in items" :key="i" :style="{ order: i + 2 }">
<router-link
:to="{ query: { ...$route.query, page: item } }"
:class="{ 'pointer-events-none': item == page }"
>
<Button :label="item" link :disabled="item == page" />
</router-link>
</li>
<li v-show="pages > 5" :style="{ order: pages / 2 > page ? 6 : 2 }">
<span>...</span>
</li>
</ul>
</template>
<script setup>
const props = defineProps(['pages'])
const route = useRoute()
const page = computed(() => +(route.query.page || 1))
const items = ref([])
const init = () => {
items.value = [page.value]
let temp = 1
while (items.value.length < 5) {
if (page.value - temp > 0) items.value.unshift(page.value - temp)
if (page.value + temp <= props.pages) items.value.push(page.value + temp)
temp++
if (temp > 10) break
}
if (props.pages > 5) {
if (props.pages / 2 > page.value) items.value.push(props.pages)
else items.value.unshift(1)
}
}
init()
watch(() => page.value, init)
const getIcon = (i) => 'isax isax-arrow-' + (i > 0 ? 'right-3' : 'left-2')
</script>
<style lang="scss">
.pagination {
box-shadow: 0px 2px 20px rgba(1, 86, 153, 0.08);
@apply bg-white rounded-lg gap-2 lg:gap-4 flex flex-row-reverse items-center justify-between px-2 lg:px-4;
@apply w-auto h-10 lg:h-[3.7rem];
li {
@apply h-full flex items-center;
button {
@apply w-5 h-5 lg:w-6 lg:h-6 p-0 rounded-sm #{!important};
.p-button-label {
@apply text-sm lg:text-base font-bold font-vazir lg:leading-tight text-[#8C8C8C] lg:mt-0.5;
}
.p-button-icon {
@apply text-[#8C8C8C] lg:mt-[0.2rem] text-xs;
}
&[disabled] {
@apply bg-[#379956] opacity-100;
.p-button-label {
@apply text-white;
}
}
// &:has(.p-button-icon){
// @apply max-lg:bg-[#F2F2F2];
// }
}
& > span {
@apply text-[#8C8C8C] font-medium font-vazir leading-[1.7rem] mt-0.5;
}
&:nth-child(2) {
@apply order-last;
}
}
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<template>
<div class="about-us">
<div>
<img src="/images/logo.svg" alt="logo" />
<p>{{ $t("aboutUsDesc") }}</p>
</div>
<ul>
<li v-for="item in items" :key="item">
<i :class="`isax isax-${item.icon}`" />
<span>{{ $t(item.title) }}</span>
</li>
</ul>
</div>
</template>
<script setup>
const items = reactive([
{ icon: "call", title: "aboutUsPhone" },
{ icon: "sms", title: "aboutUsPostalCode" },
{ icon: "location", title: "aboutUsAddress" },
]);
</script>
<style lang="scss">
footer .about-us {
@apply flex flex-col gap-6 p-8 font-vazir;
@apply w-full h-auto bg-zinc-100 rounded-2xl border-2 border-neutral-200;
@apply sm:w-[32.7rem] sm:h-[31.3rem];
@apply lg:flex-row lg:w-[57.7rem] lg:h-auto;
@apply xl:flex-col xl:w-[32.7rem] xl:h-[31.3rem];
&>div {
@apply flex flex-col gap-6;
img {
@apply w-60 h-10;
}
p {
@apply w-auto sm:w-[28.1rem] text-zinc-800 text-lg leading-[1.9rem];
}
}
ul {
@apply flex flex-col gap-4 mt-4;
li {
@apply flex gap-3 h-max;
i {
@apply text-2xl h-[1.9rem];
}
span {
@apply text-zinc-800 text-base font-medium leading-[1.9rem];
}
}
}
}
</style>
@@ -0,0 +1,43 @@
<template>
<div class="customer-services">
<h2>{{ $t("customer-services") }}</h2>
<ul>
<li v-for="(service, i) in services" :key="i">
<a :href="service.url">
<span>{{ $t(service.title) }}</span>
</a>
</li>
</ul>
</div>
</template>
<script setup>
const services = reactive([
{ url: "/contact-us", title: "contactUs" },
{ url: "/about-us", title: "aboutUs" },
{ url: "/search", title: "shop" },
{ url: "/blog", title: "media" },
]);
</script>
<style lang="scss">
footer .customer-services {
@apply flex flex-col gap-4 w-max font-vazir items-center sm:items-start;
h2 {
@apply text-zinc-800 text-xl font-bold leading-[1.9rem];
}
ul {
@apply flex gap-4 sm:flex-col justify-between sm:gap-2;
li {
a {
span {
@apply text-stone-500 text-[0.9em] leading-[1.9rem] hover:text-primary-600;
}
}
}
}
}
</style>
+32
View File
@@ -0,0 +1,32 @@
<template>
<footer>
<AppDesktopFooterAboutUs />
<div>
<AppDesktopFooterNewsletters />
<AppDesktopFooterSocialNetworks />
<div>
<AppDesktopFooterCustomerServices />
<AppDesktopFooterPurchaseGuide />
<AppDesktopFooterTrustSymbols />
</div>
</div>
</footer>
</template>
<script setup></script>
<style lang="scss">
footer {
@apply w-full flex flex-col sm:items-center justify-center bg-[#F5F5F5];
@apply gap-4 px-3 py-12;
@apply xl:flex-row xl:gap-[6.7rem] 2xl:py-[6.3rem];
&>div:not([class]) {
@apply flex flex-col gap-6;
&>div:not([class]) {
@apply flex flex-col items-center sm:items-start sm:flex-row justify-center gap-8 mt-4 lg:justify-between xl:gap-6 xl:mt-10;
}
}
}
</style>
@@ -0,0 +1,81 @@
<template>
<div class="newsletters">
<h2>{{ $t("newslettersDesc") }}</h2>
<form>
<div>
<i class="isax isax-sms" />
<span>|</span>
<input v-model="email" :class="{ '!ltr': email }" :placeholder="$t('emailAddress')" />
</div>
<Button :label="$t('submit')" :loading="loading" @click="submit()" />
</form>
</div>
</template>
<script setup>
const { toast, t } = inject('service')
const loading = ref(false)
const email = ref()
const submit = async () => {
if (email.value.includes('@')) {
loading.value = true;
const { status, error } = await useFetch('/api/news', { method: 'post', body: { email: email.value } })
loading.value = false;
email.value = '';
if (status.value == 'success')
toast.add({
life: 3000,
severity: 'success',
summary: t('success'),
detail: t('submitedSuccessfully')
})
else
toast.add({
life: 3000,
severity: 'error',
summary: t('error'),
detail: error.value
})
}
}
</script>
<style lang="scss">
footer .newsletters {
@apply bg-zinc-100 rounded-xl border-2 border-neutral-200;
@apply flex flex-col items-center justify-between p-3 font-vazir;
@apply w-full h-auto gap-4 py-4;
@apply lg:w-[57.7rem] lg:h-[4.5rem] lg:flex-row;
h2 {
@apply text-zinc-800 text-lg font-medium leading-[1.9rem] mr-3;
}
form {
@apply flex gap-2;
div {
@apply w-[16.9rem] md:w-[21.5rem] h-12 bg-neutral-200 rounded-lg flex gap-3 items-center p-3;
i {
@apply text-2xl;
}
input {
@apply h-12 bg-transparent w-full rtl text-neutral-600 font-medium outline-none;
&:placeholder {
@apply text-neutral-600;
}
}
}
button {
@apply w-[4.5rem] h-12 bg-[#47B556] rounded-md text-white text-base font-medium #{!important};
}
}
}
</style>
@@ -0,0 +1,42 @@
<template>
<div class="purchase-guide">
<h2>{{ $t("purchaseGuideFromAM") }}</h2>
<ul>
<li v-for="(guide, i) in guides" :key="i">
<a :href="guide.url">
<span>{{ $t(guide.title) }}</span>
</a>
</li>
</ul>
</div>
</template>
<script setup>
const guides = reactive([
{ url: "/", title: "howToPlaceOrder" },
{ url: "/", title: "orderSendingProcedure" },
{ url: "/", title: "paymentMethods" },
]);
</script>
<style lang="scss">
footer .purchase-guide {
@apply flex flex-col gap-4 sm:gap-[1.4rem] w-max font-vazir items-center sm:items-start;
h2 {
@apply text-zinc-800 text-xl font-bold leading-[1.9rem];
}
ul {
@apply flex gap-4 sm:flex-col sm:gap-2;
li {
a {
span {
@apply text-stone-500 text-[0.9em] leading-[1.9rem] hover:text-primary-600;
}
}
}
}
}
</style>
@@ -0,0 +1,48 @@
<template>
<div class="social-networks">
<h2>{{ $t("followUs") }}</h2>
<ul>
<li v-for="({url, icon}, i) in socials" :key="i">
<a :href="url" target="_blank">
<img :src="`/icons/${icon}.svg`" />
</a>
</li>
</ul>
</div>
</template>
<script setup>
const socials = reactive([
{ url: "/", icon: "linkedin" },
{ url: "/", icon: "telegram" },
{ url: "/", icon: "whatsapp" },
{ url: "/", icon: "instagram" },
]);
</script>
<style lang="scss">
footer .social-networks {
@apply bg-zinc-100 rounded-xl border-2 border-neutral-200;
@apply flex flex-col items-center justify-between px-6;
@apply w-full h-auto gap-4 py-4;
@apply lg:w-[57.7rem] lg:h-[4.5rem] md:flex-row;
h2 {
@apply text-zinc-800 text-lg font-medium font-vazir leading-[1.9rem];
}
ul {
@apply flex gap-2;
li {
@apply w-max h-max;
a {
@apply w-10 h-10 bg-[#B2B2B2] hover:bg-[#43E97B] rounded-full flex items-center justify-center;
img {
@apply h-6 w-6 object-scale-down;
}
}
}
}
}
</style>
@@ -0,0 +1,37 @@
<template>
<div class="trust-symbols">
<h2>{{ $t('symbolOfTrust')}}</h2>
<div>
<picture v-for="(symbol, i) in symbols" :key="i">
<img :src="`/images/${symbol}.svg`" />
</picture>
</div>
</div>
</template>
<script setup>
const symbols = reactive(['samandehi', 'kasbokar'])
</script>
<style lang="scss">
footer .trust-symbols{
@apply flex flex-col items-center gap-4;
@apply sm:items-start;
h2{
@apply text-zinc-800 text-xl font-bold font-vazir leading-[1.9rem];
}
div{
@apply flex gap-4;
picture{
@apply flex w-24 h-32 bg-white rounded-lg border border-neutral-300;
img{
@apply m-auto;
}
}
}
}
</style>
+99
View File
@@ -0,0 +1,99 @@
<template>
<header class="header-desktop">
<router-link to="/">
<img src="/images/logo.svg" alt="Asan Market" />
</router-link>
<AppDesktopHeaderNavigation />
<router-link v-if="!logged" :to="{ path: '/auth/login' }">
<Button v-bind="btns.login.props" />
</router-link>
<div v-else>
<template v-for="({ props, component }, key) in btns.logged" :key="key">
<Button v-bind="props" @click="overlay($event, component)" />
</template>
</div>
</header>
</template>
<script setup>
import init from '../../../../initialize/header'
const { btns } = init()
const logged = useCookie('token')
const { popup } = inject('service')
const overlay = (event, is) => {
popup.value.is = is
popup.value.toggle(event)
}
</script>
<style lang="scss">
.header-desktop {
box-shadow: 0px 2px 20px rgba(0.22, 75.27, 129.62, 0.14);
@apply w-full h-[3.3rem] lg:h-24 bg-white shadow border-b pl-3 lg:px-3 2xl:p-6 flex items-center justify-between;
@apply sticky top-0 z-10 xl:gap-4;
&>a>img {
@apply w-60 h-10;
}
&>a {
@apply shrink-0;
&>button {
@apply flex items-center text-white;
.p-button-icon {
@apply text-2xl;
}
.p-button-label {
@apply font-bold font-iran-sans tracking-widest whitespace-nowrap;
}
}
}
&>div {
@apply flex items-center gap-6 2xl:gap-12 relative;
&::after {
@apply content-['_'] w-0.5 h-[1.3rem] bg-[#DCDCDC] absolute left-[3.6rem] 2xl:left-[4.4rem];
}
button {
@apply border-[#CCCCCC] h-12 rounded-[0.6rem] #{!important};
.p-button-label {
@apply text-[#333333] text-lg font-medium font-vazir whitespace-nowrap;
}
.p-button-icon {
@apply text-[#333333] text-2xl;
}
&:first-child {
@apply pl-6 #{!important};
.p-button-icon {
@apply ml-2.5;
}
}
&:last-child {
@apply w-12 p-2 relative;
.p-badge {
@apply absolute w-4 h-4 bg-[#B3261E] top-0.5 right-0.5 flex items-center justify-center;
@apply text-white text-[0.7em] font-medium leading-none tracking-wide;
}
}
}
.p-blockui-container {
@apply absolute;
}
}
}
</style>
+189
View File
@@ -0,0 +1,189 @@
<template>
<Transition>
<nav v-if="visible" class="mega-menu" @click.self="visible = false">
<TransitionGroup name="list" tag="section">
<template v-for="(list, i) in menu" :key="i">
<ul v-if="list?.length">
<li v-for="(item, j) in list" :key="j" @mouseover="open(item, i)">
<router-link :to="{ path: '/search', query: { category: item.link } }">
<Button v-bind="getProps(item, i)" />
</router-link>
</li>
</ul>
</template>
</TransitionGroup>
</nav>
</Transition>
</template>
<script setup>
const visible = defineModel()
const store = useCategoriesStore()
const menu = reactive([])
const getProps = (item, i) => {
const temp = { label: item.name, link: true }
if (i === 0) {
temp.iconPos = 'right'
temp.icon = 'isax ' + item.icon
// temp.disabled = menu[1] == item.sub
} else if (i === 1 && item.sub.length > 0) {
temp.icon = 'isax isax-arrow-left-2'
// temp.disabled = menu[2] == item.sub
}
return temp
}
const open = (item, i) => {
if (i < 2) {
delete menu[i + 2]
menu[i + 1] = item.sub
}
}
onMounted(() => {
let cat = store.items
while (true) {
menu.push(cat)
if (cat[0]?.sub) cat = cat[0].sub
else break
}
})
const route = useRoute()
watch(() => route.query, () => visible.value = false)
</script>
<style lang="scss">
.mega-menu {
@apply fixed inset-x-0 z-[1] bottom-0 top-24 bg-black/40;
&>section {
@apply flex h-[32.4rem] w-full bg-white shadow-inner;
ul:nth-of-type(1) {
@apply flex flex-col gap-2 py-3 px-4 w-[16.5rem] h-[26.8rem] z-[10];
li {
button {
@apply w-full h-12 justify-end;
.p-button-label {
@apply text-[#292D32] text-lg font-medium font-vazir grow-0;
}
.p-button-icon {
@apply text-[#292D32] text-2xl ml-4;
}
&:hover * {
@apply text-primary-600;
}
&:disabled {
@apply opacity-100;
* {
@apply text-primary-600;
}
}
}
}
}
ul:nth-of-type(2) {
@apply border-x-2 border-[#E5E5E5] w-[16.5rem] h-[32.4rem] flex flex-col;
li {
@apply border-b border-[#E5E5E5];
button {
@apply w-full h-[3.7rem] justify-between px-2.5;
.p-button-label {
@apply text-[#292D32] text-lg font-vazir grow-0 ml-auto;
}
.p-button-icon {
@apply text-[#292D32] text-sm transition-transform;
}
&:hover * {
@apply text-primary-600;
}
&:disabled {
@apply opacity-100 bg-[#FAFAFA];
.p-button-label {
@apply text-[#333333] font-medium;
}
.p-button-icon {
@apply text-[#333333] -rotate-90;
}
}
}
}
}
ul:nth-of-type(3) {
@apply flex flex-col;
li {
@apply px-4;
button {
@apply w-full h-[3.8rem] justify-end;
.p-button-label {
@apply text-[#333333] text-lg font-normal font-vazir grow-0;
}
&:hover * {
@apply text-primary-600;
}
}
}
}
}
.list-enter-active,
.list-leave-active {
transition: all 0.2s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.menu-enter-active,
.menu-leave-active {
transition: all 0.3s ease;
div {
transition: all 0.3s ease;
}
}
.menu-enter-from,
.menu-leave-to {
opacity: 0;
div {
transform: translateX(30px);
}
}
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.1s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
@@ -0,0 +1,70 @@
<template>
<ul class="mini-menu">
<li v-for="(item, i) in items" :key="i">
<router-link :to="item.to" @click="popup.hide()">
<Button v-bind="item.props" iconPos="right" severity="secondary" link />
</router-link>
</li>
<li>
<Button :label="$t('logout')" icon="isax isax-logout-1" iconPos="right" severity="secondary" link
@click="logout()" />
</li>
</ul>
</template>
<script setup>
const items = reactive([
{
to: "/panel/profile",
props: { label: "پروفایل کاربری", icon: "isax isax-user-square" },
},
{ to: "/panel/favorites", props: { label: "علاقه مندی ها", icon: "isax isax-heart" } },
{
to: "/panel/comments",
props: { label: "نظرات من", icon: "isax isax-message-text4" },
},
{ to: "/panel/factors", props: { label: "فاکتورها", icon: "isax isax-receipt-2-1" } },
{
to: "/panel/moeen",
props: { label: "فاکتور معین", icon: "isax isax-archive-book4" },
},
{ to: "/panel/notifications", props: { label: "اعلان ها", icon: "isax isax-notification" } },
{
to: "/panel/orders",
props: { label: "سفارشات", icon: "isax isax-shopping-cart" },
},
])
const { popup } = inject('service')
const { dialog } = inject('service')
const logout = () => {
popup.value.hide();
dialog.show(resolveComponent('PanelDialogLogout'))
}
</script>
<style lang="scss">
.mini-menu {
@apply flex flex-col gap-3 px-4 pt-[1.4rem] rtl shadow-[0px_2px_10px_#004b8229];
@apply w-[13.4rem] h-[27.8rem] bg-white rounded-2xl shadow border;
li {
button {
@apply h-10 py-1.5 justify-end #{!important};
.p-button-icon {
@apply text-[#333333] text-2xl ml-3;
}
.p-button-label {
@apply text-[#333333] text-lg font-medium font-vazir grow-0 whitespace-nowrap;
}
}
}
}
.p-overlaypanel:has(.mini-menu) {
@apply -translate-x-8;
}
</style>
@@ -0,0 +1,97 @@
<template>
<ul class="navigation">
<li>
<Button :label="$t('product-categories')" iconPos="right" severity="secondary" text icon="isax isax-menu-1"
@click="visible = !visible" />
</li>
<li v-for="(item, i) in items" :key="i">
<component :is="item.link.includes('http') ? 'a' : 'router-link'" :to="item.link" :href="item.link">
<Button iconPos="right" severity="secondary" text v-bind="item.props" />
</component>
</li>
<li>
<Button iconPos="right" severity="secondary" text icon="isax isax-search-normal-1"
@click="$router.push('/search')" />
</li>
</ul>
<AppDesktopHeaderMegaMenu v-model="visible" />
</template>
<script setup>
const items = ref([])
const visible = ref(false)
const { status, data, error } = await useFetch('/api/headers')
if (status.value == 'success')
items.value = data.value.map((item) => {
item.props = { label: item.title, icon: 'isax ' + item.icon }
return item
})
else throw createError(error.value)
</script>
<style lang="scss">
.navigation {
@apply flex items-center justify-center lg:gap-6 2xl:justify-between w-full max-w-[70.6rem];
li:first-child,
li:last-child {
@apply flex items-center relative;
}
li:first-child::after,
li:last-child::before {
@apply content-['_'] w-0.5 h-[1.3rem] bg-[#DCDCDC] absolute;
}
li:first-child {
@apply -ml-1;
&::after {
@apply left-0 2xl:-left-3;
}
}
li:last-child {
@apply -mr-1;
&::before {
@apply 2xl:-right-3;
}
}
li {
button {
.p-button-icon {
@apply text-[#333333] text-xl;
}
&:not(.p-button-icon-only) .p-button-icon {
@apply ml-2;
}
.p-button-label {
@apply text-[#333333] text-lg font-medium font-vazir whitespace-nowrap no-underline;
}
}
}
li:nth-child(n + 7):not(:last-child) {
@apply max-[1500px]:hidden;
}
li:nth-child(n + 6):not(:last-child) {
@apply max-[1380px]:hidden;
}
li:nth-child(n + 5):not(:last-child) {
@apply max-xl:hidden;
}
li:not(:last-child:first-child) {
@apply max-2xl:-mx-1;
}
}
</style>
@@ -0,0 +1,57 @@
<template>
<div class="header-cart-list-item">
<span>{{ quantity }}x</span>
<img :src="data.coverPath" />
<div>
<h3>{{ data.category[data.category.length - 1]?.name }}</h3>
<h2>{{ data.title }}</h2>
<span>
<b>{{ numberFormat(data.specialPrice || data.price) }} </b>
{{ $t('priceUnit') }}
</span>
</div>
<Button icon="isax isax-trash" severity="secondary" text @click="$emit('remove', data.id)" />
</div>
</template>
<script setup>
defineProps(['data', 'quantity'])
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.header-cart-list-item {
box-shadow: 0px 2px 12px #004b8226;
@apply flex p-2 pl-6 w-[26.9rem] h-[10.1rem] bg-white rounded-lg shadow border relative;
&>span {
@apply absolute left-5 top-4 text-xs font-poppins;
}
img {
@apply w-[9.2rem] h-[9.2rem] p-2;
}
div {
@apply w-max gap-1 flex flex-col py-4 font-vazir mr-6;
h3 {
@apply text-right text-neutral-400 text-[0.9em] font-medium;
}
h2 {
@apply w-[13.1rem] h-14 text-right text-zinc-800 text-[1.1em] font-bold line-clamp-2;
}
span {
@apply text-zinc-800 text-[0.9em] mt-auto;
}
}
button {
@apply w-10 h-10 mb-2 -mx-2 shrink-0 self-end text-2xl text-[#7F7F7F] rounded-full #{!important};
}
}
</style>
@@ -0,0 +1,64 @@
<template>
<div class="header-cart-list">
<h3>{{ $t('itemsSubmittedInCart', { count: cart.count }) }}</h3>
<div>
<ul>
<li v-for="({ product, quantity }, i) in items" :key="i">
<AppDesktopHeaderCartItem :data="product" :quantity="quantity" @remove="remove" />
</li>
</ul>
</div>
<Button :label="$t('showCart')" @click="show()" />
</div>
</template>
<script setup>
const { popup } = inject('service')
const cart = useCartStore()
const items = computed(() => cart.items)
const emit = defineEmits(['hide'])
const router = useRouter()
const show = () => {
popup.value.hide()
router.push('/checkout/cart')
}
const remove = async (id) => {
await cart.update(id)
if (cart.count < 1) popup.value.hide()
}
</script>
<style lang="scss">
.header-cart-list {
box-shadow: 0px 2px 10px 0px rgba(0, 75, 130, 0.16);
@apply rtl w-[30.9rem] h-[40.6rem] flex flex-col gap-5 py-6 pl-2 bg-white rounded-2xl border overflow-hidden relative;
&>h3 {
@apply text-sky-700 text-[0.9em] font-medium font-vazir mb-2.5 mr-8 ml-6;
}
&>div {
@apply overflow-y-auto h-[29.8rem];
ul {
@apply w-full flex py-3 pr-8 pl-4 flex-col gap-3 items-center;
}
&::-webkit-scrollbar {
@apply hidden;
}
}
&>button {
@apply bg-sky-700 border-sky-700 mr-8 ml-6 text-xl font-bold font-vazir #{!important};
}
&::before {
content: ' ';
box-shadow: 0px -3px 16px 0px rgba(7, 50, 115, 0.16);
@apply absolute inset-x-0 -bottom-3 h-28 z-0;
}
}
</style>
+40
View File
@@ -0,0 +1,40 @@
<template>
<ul class="drawer-menu">
<li v-for="(item, i) in items" :key="i">
<router-link :to="item.link" @click="sidebar.visible = false">
<Button v-bind="item.props" iconPos="right" severity="secondary" text />
</router-link>
</li>
</ul>
</template>
<script setup>
const props = defineProps(['menu'])
const { sidebar } = inject('service')
const items = ref([])
items.value = props.menu.map((item) => {
item.props = { label: item.title, icon: 'isax ' + item.icon }
return item
})
</script>
<style lang="scss">
.drawer-menu {
@apply rtl w-60 flex flex-col gap-3 px-4 pt-[1.4rem];
li {
button {
@apply h-10 py-1.5 justify-end;
.p-button-icon {
@apply text-[#333333] text-2xl ml-3;
}
.p-button-label {
@apply text-[#333333] text-lg font-medium font-vazir grow-0 whitespace-nowrap;
}
}
}
}
</style>
+51
View File
@@ -0,0 +1,51 @@
<template>
<header class="header-mobile">
<div>
<Button v-if="h.back" v-bind="btns.back.props" @click="$router.go(-1)" />
<Button v-if="h.menu" v-bind="btns.menu.props" @click="sidebar.visible = true" />
<h2 v-if="h.title" v-text="h.title" />
<AppMobileSearchBox v-if="h.search" />
<span />
<template v-for="(opt, i) in h.options" :key="i">
<Button v-bind="opt.props" @click="opt.click" />
</template>
</div>
<AppMobileToolbar v-if="h.toolbar" />
</header>
</template>
<script setup>
const { btns, navigation } = inject('mobile')
const { sidebar } = inject('service')
const { mobile } = inject('config')
const h = computed(() => mobile.header)
</script>
<style lang="scss">
.header-mobile {
box-shadow: 0px 2px 20px #004b8224;
@apply w-full h-max bg-white shadow border-b pl-3 flex flex-col sticky top-0 z-10;
&>div {
@apply h-[3.3rem] flex items-center;
&>button {
@apply flex items-center text-[#292D32] shrink-0;
.p-button-icon {
@apply text-2xl;
}
}
&>h2{
@apply text-[#333333] text-sm font-medium font-vazir;
}
&>span {
@apply mr-auto;
}
}
}
</style>
+48
View File
@@ -0,0 +1,48 @@
<template>
<nav class="bottom-navigation">
<ul>
<li v-for="(item, i) in navigation" :key="i">
<router-link :to="item.to" v-ripple>
<Button v-bind="item.props" />
</router-link>
</li>
</ul>
</nav>
</template>
<script setup>
const { navigation } = inject('mobile');
const route = useRoute()
Object.keys(navigation).forEach(key => {
const item = navigation[key]
if (item.to == route.path)
item.props.icon += '5'
});
</script>
<style lang="scss">
.bottom-navigation {
@apply fixed inset-x-0 bottom-0 h-16 pb-2 bg-white z-10;
@apply shadow-[0px_-2px_5px_#0000000a] border-t border-[#F1F2F4];
ul {
@apply w-full h-full flex justify-between items-center;
li {
@apply flex justify-center flex-1;
a {
button span {
@apply text-[#7F7F7F] text-2xl;
}
&.router-link-active button span {
@apply text-primary-600;
}
}
}
}
}
</style>
+44
View File
@@ -0,0 +1,44 @@
<template>
<div class="search-box" @click="active = true">
<i class="isax isax-search-normal-1" />
<template v-if="active">
<InputText v-model="q" :placeholder="$t('searchIn')" autofocus @blur="active = false"
@keypress.enter="search" />
</template>
<template v-else>
<span>{{ $t("searchIn") }}</span>
<img src="/images/logo-search.svg" />
</template>
</div>
</template>
<script setup>
const route = useRoute()
const router = useRouter()
const active = ref(false);
const q = ref(route.query.q || '');
const search = () => router.push({ path: '/search', query: { q: q.value } })
</script>
<style lang="scss">
.search-box {
@apply grow flex gap-1.5 items-center h-10 bg-[#F2F2F2] rounded-[0.4rem] px-3;
input {
@apply h-full text-xs px-0 w-auto bg-transparent border-0 rtl focus:border-0 focus:shadow-none #{!important};
}
i {
@apply text-lg ml-1.5;
}
span {
@apply text-[#5D5D5D] text-xs font-vazir;
}
img {
@apply w-24 h-5;
}
}
</style>
+44
View File
@@ -0,0 +1,44 @@
<template>
<ul>
<li v-for="({ label, link }, i) in items" :key="i">
<router-link :to="link">
<span>{{ label }}</span>
</router-link>
</li>
</ul>
</template>
<script setup>
const items = reactive([
{ link: '/panel/seller/store', label: 'فروشگاه', icon: 'isax isax-shop' },
{ link: '/panel/seller/products', label: 'همه کالاها', icon: 'isax isax-bag' },
{ link: '/panel/seller/new-product', label: 'معرفی کالا', icon: 'isax isax-bag-cross' },
{ link: '/panel/seller/orders', label: 'سفارشات', icon: 'isax isax-bag-tick-2' },
])
</script>
<style lang="scss">
.header-mobile>ul {
@apply w-full h-8 flex gap-6 px-6;
li {
@apply translate-y-px;
a {
@apply h-full w-max text-[#333333] text-xs font-vazir;
&.router-link-active {
@apply text-[#47B556] font-bold;
}
}
&:has(.router-link-active) {
@apply border-b-2 border-[#47B556];
}
}
}
</style>
+156
View File
@@ -0,0 +1,156 @@
<template>
<form class="otp-code">
<h1>{{ $t("enterReceivedCode") }}</h1>
<ul ref="list">
<li v-for="(_, i) in 6" :key="i">
<InputNumber v-model="code[i]" @input="events.input($event, i)" @keyup="events.keyup($event, i)"
@focus="events.focus($event, i)" :disabled="inputs.length < 1" />
</li>
</ul>
<span :class="{ invisible: seconds < 1 }">
{{ $t("moreSecondReceiveCode", { seconds }) }}
</span>
<Button v-bind="btns.next.props" @click="events.next()" />
<div v-show="seconds < 1">
<p>{{ $t("notReceivedCode") }}</p>
<Button v-bind="btns.try.props" @click="events.try()" />
</div>
</form>
</template>
<script setup>
import init from "../../initialize/auth/otp-code.js";
const { btns } = init();
const { t, toast } = inject('service')
let inputs = reactive([]);
const list = ref();
const code = ref([]);
const seconds = ref(0);
const form = computed(() => code.value.join(""));
const store = useOtpCodeStore();
const router = useRouter();
const route = useRoute();
const events = reactive({
next: async () => {
btns.next.props.loading = true
if (form.value.length == 6) {
const { status, error } = await store.check(form.value);
if (status.value == "success")
router.push({ query: { step: 2 } });
else {
code.value = []
let detail = t("error-500");
switch (error.value.statusCode) {
case 400:
detail = t("incorrect-code");
break;
case 429:
detail = t("error-429");
break;
}
toast.add({ severity: "error", summary: t("error"), detail, life: 3000 });
}
}
btns.next.props.loading = false
},
try: async () => {
btns.try.props.loading = true
const form = { number: store.number };
let result = {}
if (route.path.includes('register'))
result = await store.withoutToken(form)
else if (route.path.includes('restore'))
result = await store.forgotPassword(form
)
const { status, error } = result;
if (status.value == "success")
seconds.value = 60;
else
toast.add({ severity: "error", summary: t("error"), detail: error.value.message, life: 3000 });
btns.try.props.loading = false
},
input: (e, i) => {
if (e.value != null) inputs[i++].value = e.originalEvent.key;
if (i < 6) inputs[i].focus();
else {
inputs[5].blur();
events.next();
}
},
keyup: (e, i) => {
if (e.code == "Backspace" && i > 0) {
code.value[i - 1] = null;
inputs[i - 1].focus();
}
},
focus: (e, i) => {
const length = code.value.filter((c) => typeof c == "number").length;
if (length < i) inputs[length].focus();
},
});
onMounted(() => {
inputs.push(...list.value.querySelectorAll("input"));
setTimeout(() => inputs[0].focus(), 1);
seconds.value = 60;
setInterval(() => {
if (seconds.value > 0) seconds.value--;
}, 1000);
});
</script>
<style lang="scss">
.auth .otp-code {
@apply flex flex-col h-max gap-3 lg:gap-4 items-center mx-auto;
&>h1 {
@apply w-full text-[#CCCCCC] font-black font-vazir text-right leading-[3rem] whitespace-nowrap;
@apply text-xl lg:text-3xl lg:text-center;
}
&>ul {
@apply flex gap-2.5 mt-3 lg:mt-8 ltr;
li {
input {
@apply w-12 lg:w-10 h-10 p-0 rounded-lg border border-stone-300 text-center;
@apply focus:text-primary-600 text-stone-300 text-xl font-medium font-vazir leading-[1.9rem];
}
}
}
&>span {
@apply text-[#999999] text-xs lg:text-sm font-medium font-vazir mt-1 lg:mt-0;
}
&>button {
@apply w-full h-11 mt-3 lg:h-14 lg:mt-6 #{!important};
.p-button-label {
@apply text-sm font-medium font-iran-sans;
}
}
&>div {
@apply flex items-center lg:mt-2 whitespace-nowrap;
p {
@apply text-[#999999] text-xs lg:text-sm font-medium font-vazir;
}
button {
@apply w-full px-1 h-11 lg:h-14 #{!important};
.p-button-label {
@apply text-sm font-medium font-iran-sans;
}
}
}
}
</style>
+177
View File
@@ -0,0 +1,177 @@
<template>
<form class="completeInformation">
<div>
<ul>
<li v-for="(item, key) in fields" :key="key">
<component :is="item.is" :id="key" 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="btn.props" @click="register()" />
</div>
</form>
</template>
<script setup>
import iranCities from "../../initialize/iranCities"
import init from "../../initialize/auth/information";
const { fields, btn } = init();
const form = reactive({});
const errors = ref({});
const otpCode = useOtpCodeStore();
onMounted(() => {
form.phoneNumber = otpCode.number
form.otp = otpCode.code
})
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)
const route = useRouter();
const router = useRouter();
const store = useUserStore();
const register = async () => {
errors.value = {}
btn.props.loading = true
const { status, error } = await store.register(Object.assign({}, form));
if (status.value == "success") {
delete store.profile.password;
router.push("/panel/profile");
} else {
if (error.value.statusCode == 422)
errors.value = error.value.data
else
toast.add({
life: 50000,
severity: 'error',
summary: t('error'),
detail: error.value.data.message || error.value.message
})
}
btn.props.loading = false
}
watch(form, () => errors.value = {})
</script>
<style lang="scss">
.auth .completeInformation {
@apply h-max flex flex-col m-auto;
@apply lg:mt-6 lg:gap-14;
&>div:nth-of-type(1) {
@apply w-max overflow-y-auto pl-4 lg:pl-7;
@apply h-[18.6rem] lg:h-[35.2rem];
&::-webkit-scrollbar-track {
@apply bg-transparent my-4;
}
&::-webkit-scrollbar {
@apply w-1 lg:w-2;
}
&::-webkit-scrollbar-thumb {
@apply bg-neutral-200;
}
ul {
@apply grid grid-cols-2 grow max-w-full py-4 px-px my-0;
@apply gap-y-6 gap-x-2 w-[21.4rem] lg:w-[27.1rem] lg:gap-x-3;
li {
@apply w-full grow h-14 relative col-span-2;
label {
@apply w-max absolute right-3 top-4 px-1 bg-white transition-['top'];
@apply text-neutral-400 text-base font-medium font-vazir cursor-text pointer-events-none;
}
input,
textarea {
@apply w-full rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
.p-dropdown-label {
@apply font-vazir mt-1;
}
&:has(input:focus),
&:has(textarea:focus),
&:has(input:not(:placeholder-shown)),
&:has(.p-dropdown-label[aria-label]),
&:has(textarea:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal font-vazir;
}
}
&:nth-of-type(1),
&:nth-of-type(2),
&:nth-of-type(3),
&:nth-of-type(4),
&:nth-of-type(8),
&:nth-of-type(9) {
@apply w-full col-span-1 max-w-[12.7rem] lg:max-w-[13.1rem];
}
.p-dropdown {
@apply w-full;
}
.p-calendar {
@apply w-full flex items-center relative z-0 font-['iconsax'] #{!important};
&::before {
@apply text-xl absolute mb-0.5 left-3 z-[1] text-[#999999];
}
input {
@apply pl-10;
}
}
small {
@apply text-red-500 font-vazir float-left;
}
&:has(small) * {
@apply border-red-500 #{!important};
}
}
}
}
&>div:nth-of-type(2) {
@apply py-3 -mr-6 px-5 w-full min-w-[24.4rem] border-t border-gray-100 shadow-[0px_-3px_5px_#00000005];
@apply lg:border-0 lg:shadow-none lg:mr-0 lg:p-0;
&>button {
@apply w-full h-11 lg:h-14 #{!important};
.p-button-label {
@apply text-sm font-medium font-iran-sans;
}
}
}
}
</style>
+108
View File
@@ -0,0 +1,108 @@
<template>
<form class="password">
<h1>{{ $t("chooseStrongPassword") }}</h1>
<ul>
<li v-for="(item, key) in fields" :key="key">
<Password v-model="form[key]" v-bind="item.props" :inputId="key" />
<label :for="key">{{ $t(key) }}</label>
</li>
</ul>
<Button v-bind="btn.props" @click="next()" />
</form>
</template>
<script setup>
import init from "../../initialize/auth/password";
const { fields, btn } = init();
const { t, toast } = inject('service')
const router = useRouter();
const route = useRoute();
const form = reactive({ password: '', repassword: null });
const store = useUserStore();
const otpCode = useOtpCodeStore()
const next = async () => {
if (form.password.length < 8) {
toast.add({ severity: "error", summary: t("error"), detail: t('passwordMin8'), life: 3000 });
} else if (form.password != form.repassword) {
toast.add({ severity: "error", summary: t("error"), detail: t('passwordNotSame'), life: 3000 });
} else {
if (route.path.includes('register')) {
store.profile.password = form.password
router.push({ query: { step: 3 } });
} else if (route.path.includes('restore')) {
btn.props.loading = true
const body = Object.assign({}, form)
body.phoneNumber = otpCode.number
body.otp = otpCode.code
delete body.repassword
const { status, error } = await store.forgetPassword(body);
if (status.value == "success") {
toast.add({ severity: "success", summary: t("success"), detail: t('updatePasswordSuccessfull'), life: 3000 });
router.replace('/auth/login');
}
else {
let detail = error.value.data.msg
toast.add({ severity: "error", summary: t("error"), detail, life: 3000 });
}
btn.props.loading = false
}
}
}
</script>
<style lang="scss">
.auth .password {
@apply flex flex-col m-auto gap-[1.9rem] lg:gap-[3.2rem];
&>h1 {
@apply w-full 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 lg:mb-7;
li {
@apply w-full 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;
}
.p-password {
input {
@apply w-full rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
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;
}
}
}
}
button {
@apply w-full h-11 mt-1.5 lg:h-14 #{!important};
.p-button-label {
@apply text-sm font-medium font-iran-sans;
}
}
}
</style>
+87
View File
@@ -0,0 +1,87 @@
<template>
<form class="phone">
<h1>{{ $t("enterYourPhoneNumber") }}</h1>
<div>
<InputText v-bind="field.props" v-model="form.number" />
<label for="phone"> {{ $t("phoneNumber") }} </label>
</div>
<Button v-bind="btn.props" @click="send()" />
</form>
</template>
<script setup>
import init from "../../initialize/auth/phone";
const { field, btn } = init();
const store = useOtpCodeStore();
const router = useRouter()
const route = useRoute()
const form = reactive({ number: store.number });
const { toast, t } = inject('service')
const send = async () => {
btn.props.loading = true;
let result = {}
const body = Object.assign({}, form);
if (route.path.includes('register'))
result = await store.withoutToken(body)
else if (route.path.includes('restore'))
result = await store.forgotPassword(body
)
const { status, data, error } = result;
btn.props.loading = false;
if (status.value == "success")
router.push({ query: { step: 1 } });
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 .phone {
@apply flex flex-col h-max m-auto;
@apply gap-[1.9rem] lg:gap-10;
&>h1 {
@apply text-[#CCCCCC] font-black font-vazir text-right whitespace-nowrap;
@apply text-xl lg:text-3xl leading-[3rem];
}
&>div {
@apply w-full 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 font-vazir cursor-text;
}
input {
@apply w-full rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
&:has(input:focus),
&:has(input:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal font-vazir;
}
}
}
button {
@apply w-full mt-1.5 h-11 lg:h-14 lg:mt-5 #{!important};
.p-button-label {
@apply text-sm font-medium font-iran-sans;
}
}
}
</style>
+53
View File
@@ -0,0 +1,53 @@
<template>
<aside class="blog-aside">
<div v-for="(items, key) in list" :key="key">
<h2>{{ $t(key) }}</h2>
<div>
<ul>
<li v-for="(item, i) in items" :key="i">
<BlogItem :data="item" type="small" />
</li>
</ul>
</div>
</div>
<BlogBanners />
</aside>
</template>
<script setup>
const { lastBlog, topBlog } = inject('data')
const list = reactive({ lastBlog, topBlog })
</script>
<style lang="scss">
.blog-aside {
@apply max-w-full flex flex-col gap-6 lg:gap-12;
@apply min-[1725px]:flex-col mx-auto;
&>div {
@apply w-full flex overflow-hidden flex-col gap-6 lg:gap-12;
h2 {
@apply flex items-center gap-3 text-[#333333] font-bold font-vazir leading-tight;
@apply text-base lg:text-2xl px-6 lg:px-0;
&::after {
@apply content-['_'] grow h-px bg-[#E5E5E5] lg:hidden;
}
}
&>div {
@apply overflow-x-auto max-w-[100vw] py-px scroll-smooth grow-0;
&::-webkit-scrollbar {
@apply hidden;
}
ul {
@apply flex lg:flex-col w-max gap-[1.1rem] lg:gap-6 px-6 lg:px-0;
}
}
}
}
</style>
+26
View File
@@ -0,0 +1,26 @@
<template>
<ul v-if="banners">
<li v-for="({ link, src }, i) in banners" :key="i">
<router-link :to="link">
<img :src="src" />
</router-link>
</li>
</ul>
</template>
<script setup>
const { banners = [] } = inject('data')
</script>
<style lang="scss">
.blog>aside>ul {
@apply flex-col gap-10 justify-end hidden lg:flex;
@apply gap-5 sm:gap-6 md:gap-7 lg:gap-8 xl:gap-9 2xl:gap-10;
li {
img {
@apply w-full max-w-[26.1rem] bg-zinc-300 rounded-[0.6rem] overflow-hidden;
}
}
}
</style>
+71
View File
@@ -0,0 +1,71 @@
<template>
<article class="post-card">
<img :src="data.coverImage" />
<div>
<small>
<router-link :to="{ path: '/blog', query: data.category.link }">
{{ data.category.title }}
</router-link>
</small>
<h1>
<router-link :to="'/blog/' + data.link">
{{ data.title }}
</router-link>
</h1>
<p v-html="data.content"></p>
<div>
<span>{{ new Date(data.createdAt).toLocaleDateString('fa') }}</span>
<router-link :to="'/blog/' + data.link">
<Button :label="$t('readMore')" icon="isax isax-arrow-left" severity="secondary" text />
</router-link>
</div>
</div>
</article>
</template>
<script setup>
defineProps({ data: { type: Object, default: {} } })
</script>
<style lang="scss">
.post-card {
box-shadow: 0px 1px 22px 0px rgba(0, 75, 130, 0.15);
@apply w-[21.3rem] h-[23.3rem] lg:w-[18.3rem] lg:h-[24.3rem] bg-white rounded-2xl p-3 flex flex-col gap-3;
img {
@apply w-full h-[10.4rem] bg-[#ECECEC] rounded-xl;
}
&>div {
@apply flex flex-col px-3 grow;
small {
@apply text-primary-600 text-xs lg:text-sm font-vazir;
}
h1 {
@apply text-[#191919] font-bold font-vazir mt-1.5 text-sm lg:text-base;
}
&>p {
@apply text-[#7F7F7F] font-vazir mt-2 w-[14.4rem] text-xs leading-5 lg:text-sm lg:leading-[1.4rem] line-clamp-3;
}
div {
@apply flex items-center justify-between mt-auto;
span {
@apply text-[#7F7F7F] text-xs lg:text-sm font-normal font-vazir leading-snug;
}
button {
@apply whitespace-nowrap h-10 -ml-3 px-2 text-xs lg:text-sm #{!important};
.isax {
@apply text-xl #{!important};
}
}
}
}
}
</style>
+67
View File
@@ -0,0 +1,67 @@
<template>
<details class="blog-categories" open>
<summary>
<h2>{{ $t('categoryOfContent') }}</h2>
<i class="isax isax-arrow-circle-down"></i>
</summary>
<ul>
<li v-for="(category, i) in blogCategory" :key="i">
<router-link :to="{ path: '/blog', query: { category: category.link } }">
<span>{{ category.name }}</span>
<i :class="{ selected: category.link == selectedCate }"></i>
</router-link>
</li>
</ul>
</details>
</template>
<script setup>
const props = defineProps(['data'])
const { blogCategory, selectedCate } = inject('data') || props.data
</script>
<style lang="scss">
.blog-categories {
@apply w-[17.1rem] lg:w-[18.3rem] appearance-none select-none shrink-0 rtl max-xl:hidden;
@apply h-full bg-white m-0 max-h-none ml-auto 2xl:ml-6 #{!important};
summary {
@apply w-full h-[3.9rem] lg:h-[5.5rem] flex items-center justify-between bg-[#43E97B] lg:rounded-[0.6rem] p-4 cursor-pointer;
h2 {
@apply text-stone-50 text-lg lg:text-xl font-bold font-vazir leading-[2.8rem];
}
i {
@apply text-white text-[1.4em] lg:text-2xl transition-transform;
}
}
&[open] summary i {
@apply rotate-180;
}
ul {
@apply flex flex-col gap-3 mt-[1.1rem] lg:gap-4 lg:mt-4 px-3 lg:px-0;
li {
a {
@apply w-full h-14 bg-[#F8F8F8] rounded-[0.6rem];
@apply flex items-center justify-between px-[1.1rem] lg:p-4 hover:bg-stone-100;
span {
@apply text-neutral-400 text-sm lg:text-xl font-medium font-vazir leading-[1.9rem];
}
i {
@apply w-2.5 h-2.5 lg:w-3 lg:h-3 bg-[#D9D9D9] rounded-full;
}
.selected {
@apply bg-[#43E97B];
}
}
}
}
}
</style>
+63
View File
@@ -0,0 +1,63 @@
<template>
<article>
<router-link :to="'/blog?category=' + category.link">
{{ category.name }}
</router-link>
<h1>{{ title }}</h1>
<span>{{ $t('author') }}: {{ userID.firstName }} {{ userID.lastName }}</span>
<div>
<i class="isax isax-calendar-1"></i>
<span>{{ new Date(createdAt).toLocaleString('fa') }}</span>
</div>
<img :src="coverImage" :alt="title" />
<div v-html="content" />
</article>
</template>
<script setup>
const data = inject('data')
const { category, userID, createdAt, coverImage, title, content } = data.post
</script>
<style lang="scss">
.blog-post>section>article {
@apply w-full max-lg:container max-w-[64.6rem] flex flex-col max-lg:px-6;
&>a {
@apply text-primary-600 font-vazir w-max hover:underline;
@apply text-xs lg:text-2xl lg:leading-[2.7rem] mt-3 lg:mt-0;
}
&>h1 {
@apply text-[#404040] font-bold font-vazir;
@apply text-base lg:text-3xl mt-2 lg:mt-6;
}
&>span {
@apply text-[#BFBFBF] font-normal font-vazir;
@apply text-sm lg:text-xl lg:leading-9 mt-[1.1rem] lg:mt-8;
}
&>div:first-of-type {
@apply flex items-center gap-1.5 lg:gap-2 text-[#BFBFBF] mt-2;
&>span {
@apply font-normal font-vazir;
@apply text-sm lg:text-xl;
}
i {
@apply text-[1.4em] lg:text-2xl;
}
}
&>img {
@apply w-full bg-zinc-300 order-first lg:order-none lg:mt-6;
}
&>div:last-of-type {
@apply text-[#595959] font-vazir mt-9 lg:mt-12 text-justify;
@apply text-sm leading-6 lg:text-xl lg:leading-9;
}
}
</style>
+124
View File
@@ -0,0 +1,124 @@
<template>
<router-link :to="'/blog/' + link" :class="['post-item', `post-item-${type}`]">
<article>
<img :src="coverImage" :alt="title" />
<div>
<h1>{{ title }}</h1>
<div v-if="type != 'vertical'" v-html="content" />
<Button v-if="type == 'normal'" v-bind="btn.props" />
</div>
</article>
</router-link>
</template>
<script setup>
import init from '../../initialize/post-item'
const { btn } = init()
const props = defineProps({
type: { default: 'normal' },
data: {},
});
const { link, coverImage, title, content } = props.data
</script>
<style lang="scss">
.post-item {
article {
@apply flex gap-3 lg:gap-6;
img {
@apply object-contain object-center;
}
&>div {
@apply flex flex-col overflow-hidden font-vazir;
h1 {
@apply text-zinc-800 font-bold truncate;
}
p {
@apply text-zinc-500 lg:leading-[1.9rem];
}
}
}
}
.post-item-normal {
article {
@apply w-[21.4rem] h-[4.7rem];
@apply lg:w-[55.9rem] lg:h-[11.9rem];
img {
@apply w-[4.7rem] h-[4.7rem] lg:w-[15.1rem] lg:h-[11.9rem] object-cover;
}
&>div {
@apply flex flex-col justify-between lg:justify-center;
@apply gap-1 lg:gap-6;
h1 {
@apply text-xs lg:text-2xl whitespace-normal;
}
p {
@apply hidden lg:block w-[39.4rem] line-clamp-3;
}
button {
@apply w-max h-max text-[#43E97B] py-1.5 px-3.5 -mr-3.5 -my-1.5 #{!important};
.p-button-label {
@apply font-normal font-vazir text-[0.7em] lg:text-base;
}
.isax {
@apply text-base lg:text-2xl mr-1 #{!important};
}
}
}
}
}
.post-item-small {
article {
@apply w-[21.4rem] h-[4.7rem] lg:w-[26.1rem] lg:h-[7.7rem];
img {
@apply w-[4.8rem] h-[4.8rem] lg:w-[7.7rem] lg:h-[7.7rem];
}
&>div {
@apply gap-2 lg:gap-0.5 justify-center;
h1 {
@apply text-xs lg:text-base lg:leading-[2.8rem];
}
p {
@apply text-[0.7em] lg:text-base lg:w-[16.9rem] line-clamp-2;
}
button {
@apply hidden;
}
}
}
}
.post-item-vertical {
article {
@apply flex-col gap-3 lg:gap-4 w-[9.9rem] lg:w-[18.3rem];
img {
@apply w-full h-[9.9rem] lg:h-[18.3rem] object-cover;
}
&>div h1 {
@apply text-xs lg:text-base leading-6 line-clamp-3 lg:line-clamp-2 whitespace-break-spaces;
}
}
}
</style>
+28
View File
@@ -0,0 +1,28 @@
<template>
<section>
<ul>
<li v-for="(item, i) in blogs" :key="i">
<BlogItem :data="item" />
</li>
</ul>
<AppPagination :pages="pages" />
</section>
</template>
<script setup>
const { blogs, pages } = inject('data')
</script>
<style lang="scss">
.blog>section {
@apply flex flex-col justify-between px-4 lg:px-6 xl:px-0 w-full xl:w-auto items-center;
&>ul:not([class]) {
@apply flex flex-col gap-6 max-lg:w-full;
}
.pagination {
@apply self-center my-[1.9rem] lg:my-[7.4rem];
}
}
</style>
+47
View File
@@ -0,0 +1,47 @@
<template>
<section v-if="relatedPosts.length > 0" class="blog-relateds">
<div>
<h2>{{ $t("relatedPosts") }}</h2>
<Swiper slides-per-view="auto">
<SwiperSlide v-for="(item, i) in relatedPosts" :key="i">
<BlogItem :data="item" type="vertical" />
</SwiperSlide>
</Swiper>
</div>
</section>
</template>
<script setup>
const { relatedPosts = [] } = inject('data')
</script>
<style lang="scss">
.blog-relateds {
@apply w-full flex justify-center;
&>div {
@apply w-full flex flex-col gap-6 lg:gap-14;
h2 {
@apply flex gap-3 lg:gap-6 items-center text-[#333333] font-bold font-vazir;
@apply text-base lg:text-2xl lg:leading-[1.9rem] px-6;
&::before {
@apply content-['_'] w-1 h-[1.2rem] lg:h-6 lg:w-[0.3rem] rounded-full bg-primary-600;
}
}
.swiper {
@apply container px-6;
.swiper-wrapper {
@apply w-max;
.swiper-slide {
@apply w-max ml-[1.1rem] lg:ml-6 last:ml-0;
}
}
}
}
}
</style>
+74
View File
@@ -0,0 +1,74 @@
<template>
<div class="blog-share">
<i class="isax isax-hierarchy-2"></i>
<h3>{{ $t("share") }}</h3>
<ul>
<li v-for="(item, i) in items" :key="i">
<a :href="item.url" target="_blank">
<img :src="`/icons/${item.icon}.svg`" />
</a>
</li>
<li @click="copyToClipboard()">
<i class="isax isax-link-21"></i>
</li>
</ul>
</div>
</template>
<script setup>
const { post: { title } } = inject('data')
const { toast, t } = inject('service')
const route = useRoute();
const path = document.location.href;
const items = reactive([
{ icon: "linkedin", url: `https://www.linkedin.com/sharing/share-offsite/?title=${title}&url=${path}` },
{ icon: "telegram", url: `https://t.me/share/url?text=${title}&url=${path}` },
{ icon: "whatsapp", url: `https://api.whatsapp.com/send?text=${title} ${path}` },
{ icon: "twitter", url: `http://twitter.com/share?text=${title}&url${path}` },
]);
const copyToClipboard = () => {
toast.add({ life: 3000, severity: 'success', summary: t('success'), detail: t('copied') })
navigator.clipboard.writeText(path)
};
</script>
<style lang="scss">
.blog-share {
@apply w-auto lg:w-[26.8rem] lg:max-w-[26.8rem] flex items-center rounded-[0.6rem] relative max-lg:mx-6;
@apply flex-col lg:flex-row lg:h-14 lg:overflow-hidden lg:bg-zinc-100 gap-4 lg:gap-0;
&>i {
@apply text-2xl lg:hidden;
}
h3 {
@apply bg-neutral-200 w-full sm:w-[9.3rem] h-10 sm:h-full text-zinc-900;
@apply font-medium font-vazir leading-[1.9rem] items-center justify-center;
@apply text-lg hidden lg:flex;
}
ul {
@apply w-[14.3rem] flex gap-6 justify-center items-center grow h-9 text-[#333333] relative;
@apply bg-white rounded-[0.3rem] shadow-[0px_0px_10px_10px_#0000000a] z-[1];
@apply lg:shadow-none lg:h-full lg:bg-[#F2F2F2];
li {
img {
@apply w-5 h-5 lg:w-6 lg:h-6 object-scale-down object-bottom brightness-[0.45];
}
i {
@apply text-xl lg:text-[1.6em] lg:leading-9 cursor-pointer;
}
}
&::after {
@apply max-lg:content-['_'] w-4 h-4 transform rotate-45 bg-white absolute -top-2 rounded;
}
}
&::after {
@apply max-lg:content-['_'] absolute bottom-4 inset-x-0 h-0.5 bg-[#E5E5E5] z-0;
}
}
</style>
+42
View File
@@ -0,0 +1,42 @@
<template>
<div class="post-tags">
<h3>{{ $t('tags') }}:</h3>
<ul>
<li v-for="(tag, i) in post.tags" :key="i">
<router-link :to="`/blog?tag=${tag}`">
<span>{{ tag }}</span>
</router-link>
</li>
</ul>
</div>
</template>
<script setup>
const { post } = inject('data')
</script>
<style lang="scss">
.post-tags {
@apply flex items-center gap-3 lg:gap-4 px-6;
h3 {
@apply text-zinc-900 font-medium font-vazir leading-[1.9rem] whitespace-nowrap;
@apply text-xs lg:text-lg pt-1.5 lg:pt-0;
}
ul {
@apply flex flex-wrap items-center;
@apply gap-1.5 lg:gap-4;
a {
@apply bg-[#F2F2F2] rounded-[0.3rem] lg:rounded-[0.6rem] flex items-center justify-center;
@apply px-4 h-7 lg:h-[2.4rem];
span {
@apply text-[#333333] font-normal font-vazir leading-[1.9rem];
@apply text-xs lg:text-lg;
}
}
}
}
</style>
+47
View File
@@ -0,0 +1,47 @@
<template>
<section class="cart-bought-products">
<div>
<h2>{{ $t("buyersBoughtProducts") }}</h2>
<Swiper slides-per-view="auto">
<SwiperSlide v-for="(product, i) in buyers" :key="i">
<ProductCard :data="product" mode="main" />
</SwiperSlide>
</Swiper>
<HomeMostPopularThisWeek />
</div>
</section>
</template>
<script setup>
const { buyers = [] } = inject('data')
</script>
<style lang="scss">
.cart-bought-products {
@apply flex justify-center py-6 lg:py-16 bg-[#F6F8FA];
& > div {
@apply container flex flex-col gap-6 lg:gap-16;
& > h2 {
@apply text-[#333333] self-center text-lg lg:text-3xl font-bold font-vazir;
}
& > .swiper {
@apply w-full py-2 lg:py-4 ;
.swiper-wrapper {
@apply w-max;
.swiper-slide {
@apply w-max ml-3 lg:ml-5 last:ml-0;
}
}
}
& > .home-most-popular-this-week {
@apply hidden lg:flex;
}
}
}
</style>
+170
View File
@@ -0,0 +1,170 @@
<template>
<div class="cart-item">
<img :src="data.coverPath" />
<div>
<h2>{{ data.title }}</h2>
<ul>
<li v-for="(item, i) in items1" :key="i">
<i :class="`isax isax-${item.icon}`"></i>
<span v-for="(title, j) in item.title" :key="j">
{{ title }}
</span>
</li>
</ul>
<div>
<div>
<i v-for="i in 2" :key="i"></i>
</div>
<ul>
<li v-for="(item, i) in items2" :key="i">
<i :class="`isax isax-${item.icon}`"></i>
<span>{{ item.title }}</span>
</li>
</ul>
</div>
</div>
<div>
<div>
<Chip>
<bdi>{{ quantity }} کارتن = {{ data.itemInBox * quantity }} عدد</bdi>
</Chip>
<CartQuantity v-model="quantity" :range="data.orderRange" />
</div>
<span>{{ numberFormat((data.specialPrice || data.price) * quantity) }} <small>تومان</small></span>
</div>
</div>
</template>
<script setup>
const { data } = defineProps(['data'])
const items1 = reactive([
{ title: ['فروشنده ' + data.shopID.shopName], icon: 'shop' },
{
title: [
'خرید کارتنی', `حداقل ${data.orderRange?.min} کارتن`, `تعداد در کارتن: ${data.itemInBox} عدد`
],
icon: 'd-square'
},
{ title: [data.warranty], icon: 'shield-tick' },
{ title: ['موجود در انبار فروشنده'], icon: 'box-tick' }
])
const items2 = reactive([
{
title: 'ارسال از آناهیتا پس از دریافت از فروشنده', icon: 'box', primary: data.localSend
},
{
title: 'ارسال از فروشنده پس از تایید نهایی 1-2 روز دیگر', icon: 'profile-circle', primary: !data.localSend
}
])
const cart = useCartStore()
const quantity = computed({
get: () => cart.find(data.id)?.quantity || 0,
set: (v) => cart.update(data.id, v)
})
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.cart-item {
@apply w-auto flex flex-wrap justify-center items-center lg:flex-row lg:justify-between;
@apply gap-9 lg:gap-10 lg:max-w-[63.1rem] lg:max-h-[19.8rem];
&>img {
@apply object-contain object-center;
@apply w-40 h-40 lg:hidden xl:block;
@apply lg:w-[13.4rem] lg:h-[13.4rem];
}
&>div:nth-of-type(1) {
@apply flex flex-col gap-6;
&>h2 {
@apply text-[#333333] font-bold font-vazir text-base lg:text-xl;
}
&>ul {
@apply flex flex-col font-vazir gap-5 lg:gap-4 lg:mt-2.5;
li {
@apply flex items-center text-zinc-800;
@apply gap-[0.9rem] lg:gap-4 h-5 lg:h-7;
i {
@apply text-xl lg:text-2xl;
}
span {
@apply relative pl-0.5 text-xs lg:text-lg whitespace-nowrap;
&:not(:last-child)::after {
@apply content-['_'] w-px lg:w-0.5 h-4 bg-[#E5E5E5] absolute;
@apply -left-1.5 top-0.5 lg:-left-2 lg:top-1.5;
}
}
}
}
div {
@apply flex pr-0.5 lg:pr-[0.3rem] gap-4 lg:gap-6;
div {
@apply flex flex-col items-center justify-between relative;
@apply h-[3.3rem] -mt-[0.2rem] pt-[0.4rem];
@apply w-2.5 lg:h-[4.3rem] lg:pt-3.5 lg:-mt-1.5;
i {
@apply w-2.5 h-2.5 rounded-full first:bg-primary-600 last:bg-[#E5E5E5] z-0;
}
&::before {
@apply content-['_'] absolute w-0.5 inset-y-0 bg-[#E5E5E5];
}
}
ul {
@apply flex flex-col font-vazir gap-4 lg:gap-5;
li {
@apply flex gap-2 items-center h-[1.1rem] lg:h-[1.6rem];
i {
@apply text-primary-600 text-[1.1em] lg:text-xl;
}
span {
@apply text-zinc-500 text-[0.7em] lg:text-base;
}
}
}
}
}
&>div:nth-of-type(2) {
@apply w-full flex justify-around items-center;
@apply md:w-max md:flex-col-reverse md:items-end md:justify-between md:self-stretch;
&>span {
@apply whitespace-nowrap text-[#333333] font-bold font-vazir text-base lg:text-xl;
small {
@apply text-xs lg:text-inherit;
}
}
&>div {
@apply flex flex-col gap-4 flex-wrap justify-end;
&>div:nth-of-type(1) {
@apply bg-zinc-100 rounded-lg justify-center hidden lg:flex;
@apply lg:h-10 lg:w-full lg:text-base;
}
}
}
}
</style>
+40
View File
@@ -0,0 +1,40 @@
<template>
<div class="cart-quantity">
<div>
<Button icon="isax isax-add" text severity="secondary" @click="count++" :disabled="count >= range.max" />
<b>{{ count }}</b>
<Button icon="isax isax-minus" text severity="secondary" @click="count--" :disabled="count <= range.min" />
</div>
<Button icon="isax isax-trash" text severity="danger" @click="count = 0" />
</div>
</template>
<script setup>
defineProps({
range: { type: Object, default: { min: 1, max: 1000 } }
})
const count = defineModel({ default: 1 })
</script>
<style lang="scss">
.cart-quantity {
@apply flex items-center gap-3 lg:gap-4;
div {
@apply flex border border-[#E5E5E5] rounded-lg;
b {
@apply flex items-center justify-center text-primary-600;
@apply w-10 h-10 text-base lg:w-12 lg:h-12 lg:text-xl lg:pb-1;
}
button {
@apply rounded-lg text-primary-600 w-10 h-10 text-base lg:w-12 lg:h-12 lg:text-[1.8em] #{!important};
}
}
&>button {
@apply rounded-lg border border-[#E5E5E5] w-10 h-10 text-base lg:w-12 lg:h-12 lg:text-xl #{!important};
}
}
</style>
+77
View File
@@ -0,0 +1,77 @@
<template>
<section class="cart-tabs">
<ul>
<li v-for="(item, i) in tabs" :key="i">
<div :active="select == i" @click="select = i" v-ripple>
<h2>{{ $t(item) }}</h2>
<span>{{ i < 1 ? data.length : 0 }}</span>
</div>
</li>
</ul>
<Button v-if="device == 'desktop' && data.length > 0" :label="$t('removeAllFromCart')" icon="isax isax-trash"
iconPos="right" text raised severity="danger" :loading="loading" @click="clear()" />
</section>
</template>
<script setup>
const select = ref(0);
const data = inject('data')
const tabs = reactive(["cart", "previousPurchase"])
const { device } = inject('service')
const loading = ref(false)
const cart = useCartStore()
const clear = async () => {
loading.value = true
await cart.clear()
loading.value = false
}
</script>
<style lang="scss">
.cart-tabs {
@apply container w-full h-max max-lg:sticky top-0 flex justify-center lg:justify-start items-center pr-6 lg:pr-0 lg:border-b-2 border-[#E5E5E5];
@apply bg-white max-lg:z-[1];
ul {
@apply flex items-center lg:ml-[33.8rem];
li {
@apply whitespace-nowrap;
div {
@apply px-8 py-2.5 lg:px-6 lg:pb-6 flex items-center gap-3 hover:bg-gray-50 rounded-md cursor-pointer;
h2 {
@apply text-[#999999] text-lg font-normal lg:text-2xl lg:font-medium font-vazir;
}
span {
@apply w-[1.4rem] h-[1.4rem] lg:w-8 lg:h-8 bg-[#F2F2F2] rounded flex items-center justify-center;
@apply text-xs lg:text-lg font-medium font-vazir text-[#999999];
}
}
}
li:has([active="true"]) {
@apply relative;
* {
@apply text-primary-600;
}
&::after {
@apply content-['_'] inset-x-0 -bottom-[0.2rem] rounded-full h-0.5 lg:h-1 bg-primary-600 absolute;
}
}
}
button {
@apply h-[3.6rem] mb-3.5 text-[#AD3434];
.p-button-label {
@apply text-sm font-medium font-vazir
}
}
}
</style>
+108
View File
@@ -0,0 +1,108 @@
<template>
<div class="pre-factor">
<div>
<h2>
<i class="isax isax-receipt-2"></i>
<span>{{ $t("submitPreFactor") }}</span>
</h2>
<p>{{ $t("submitPreFactorDesc") }}</p>
</div>
<PanelSalesFactor :data="data" />
<ul>
<li v-for="(btn, key) in btns" :key="key">
<Button v-bind="btn.props" @click="click(key)" />
</li>
</ul>
</div>
</template>
<script setup>
import init from '../../../initialize/pre-factor.js'
const { btns } = init();
const dialog = inject('dialogRef');
const data = dialog.value.data
const router = useRouter()
const click = (key) => {
if (key == 'send')
router.push({ path: '/panel/factors', query: { id: data._id } });
dialog.value.close()
}
</script>
<style lang="scss">
.pre-factor {
@apply flex flex-col gap-8 rounded-2xl w-[77.5rem] py-8 bg-white overflow-y-auto;
&>div:nth-of-type(1) {
@apply flex flex-col items-center;
h2 {
@apply flex items-center gap-4 text-[#333333];
i {
@apply text-2xl;
}
span {
@apply text-xl font-bold font-vazir;
}
}
p {
@apply w-[30.2rem] mt-[1.1rem] text-center text-[#4C4C4C] text-lg font-vazir;
}
}
&>ul {
@apply w-full flex gap-4 px-8;
li {
button {
@apply w-full h-12 #{!important};
.p-button-label {
@apply h-[1.6rem] text-lg font-medium font-iran-sans;
}
.p-button-icon {
@apply text-2xl text-[#7F7F7F];
}
&.p-button-secondary {
@apply border-[#7F7F7F] justify-center;
.p-button-label {
@apply grow-0 text-[#7F7F7F];
}
}
}
}
li:nth-of-type(1) {
@apply w-[12.3rem];
}
li:nth-of-type(2) {
@apply w-[12.3rem] ml-auto;
}
li:nth-of-type(3) {
@apply w-[25.1rem];
}
li:nth-of-type(4) {
@apply w-[10.8rem];
}
}
}
body:has(.pre-factor) {
.sales-factor {
h2 {
@apply hidden #{!important};
}
}
}
</style>
+73
View File
@@ -0,0 +1,73 @@
<template>
<div class="cart-total-aside">
<ul>
<li v-for="(value, key) in items" :key="key">
<label>{{ $t(key) }}</label>
<span>
{{ numberFormat(value) }}
{{ key == 'productsCount' ? $t('product') : '' }}
</span>
</li>
</ul>
<Button :label="$t('submitAndViewPreFactor')" :loading="loading" @click="submitOrder()" />
</div>
</template>
<script setup>
import init from '../../../initialize/cart-total-invoice'
const { dialog } = inject('service')
const data = inject('data')
const items = computed(() => init(data.value))
const loading = ref(false)
const submitOrder = async () => {
loading.value = true
const { status, data: res } = await useFetch('/api/orders', {
headers: { Authorization: useCookie('token') }, method: 'post'
})
loading.value = false
if (status.value == 'success') {
dialog.show(resolveComponent('CartDialogPreFactor'), res.value)
const cart = useCartStore()
cart.get()
}
}
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.cart-total-aside {
@apply w-full lg:max-w-[28.1rem] lg:h-[25rem] flex flex-col justify-between p-6 grow;
@apply lg:bg-neutral-50 lg:rounded-[0.6rem] lg:border border-neutral-200 font-vazir lg:mr-auto;
ul {
@apply flex flex-col gap-[1.1rem] lg:gap-6 lg:mt-2;
li {
@apply flex justify-between items-center;
label {
@apply text-zinc-500 text-sm lg:text-xl font-medium;
}
span {
@apply text-zinc-800 text-lg lg:text-2xl lg:font-medium;
}
}
}
button {
@apply max-lg:hidden;
span {
@apply text-xl font-bold;
}
}
}
</style>
+80
View File
@@ -0,0 +1,80 @@
<template>
<div class="total-invoice-fixed">
<Button :label="$t('submitAndViewPreFactor')" @click="submitOrder()" />
<div>
<label>{{ $t('totalFactor') }}</label>
<span>{{ numberFormat(items.totalFactor) }} {{ $t('priceUnit') }}</span>
</div>
<Button icon="isax isax-trash" severity="danger" text rounded @click="remove()" />
</div>
</template>
<script setup>
import init from '../../initialize/cart-total-invoice'
const { dialog } = inject('service')
const data = inject('data')
const items = computed(() => init(data.value))
const loading = ref(false)
const submitOrder = async () => {
loading.value = true
const { status, data } = await useFetch('/api/orders', {
headers: { Authorization: useCookie('token') }, method: 'post'
})
loading.value = false
if (status.value == 'success') {
dialog.open(resolveComponent('DialogPreFactorSubmit'), {
props: { modal: true },
data
})
}
}
const remove = () => {
dialog.show()
}
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.total-invoice-fixed {
@apply fixed inset-x-0 bottom-16 h-[4.6rem] z-[11] bg-white flex justify-between items-center;
@apply shadow-[0px_-3px_5px_#0000000a] border-t border-[#F1F2F4] px-6 lg:hidden;
&>button:first-of-type {
@apply h-10 w-[10.7rem] #{!important};
.p-button-label {
@apply text-xs font-iran-sans font-bold;
}
}
&>div {
@apply flex flex-col gap-2 justify-between items-end font-vazir;
label {
@apply text-[#7F7F7F] text-xs;
}
span {
@apply text-[#333333] text-sm font-bold;
}
}
&>button:last-of-type {
@apply fixed lg:hidden w-[3.8rem] h-[3.8rem] right-6 bottom-36 z-10 bg-white #{!important};
@apply shadow-[0px_0px_10px_#00000014];
.p-button-icon {
@apply text-[2em] text-[#AD3434];
}
}
}
</style>
+152
View File
@@ -0,0 +1,152 @@
<template>
<div class="compare-selection">
<div>
<h2>{{ $t('selectProductCompare') }}</h2>
<Button icon="isax isax-close-circle" severity='secondary' rounded text @click="dialog.close()" />
</div>
<div>
<div>
<i class="isax isax-search-normal-1" />
<input v-model="query" :placeholder="$t('searchInPorducts')" />
</div>
<div>
<span>{{ $t('topProductToCompare') }}</span>
<span>{{ $t('commodity') }} {{ count }}</span>
</div>
<div>
<ul>
<li v-for="(product, i) in store.items" :key="i" @click="add(product.id)">
<ProductCard :data="product" type="main" />
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
const dialog = inject('dialogRef')
const query = ref()
const count = ref(759)
const route = useRoute()
const router = useRouter()
const store = useProductsStore()
const ids = route.params.ids.split('-')
store.compareSearch(ids)
watch(query, (v) => store.compareSearch(ids, v))
const add = (id) => {
if (!ids.includes(id)){
dialog.value.close()
router.push(`/compare/${[...ids, id].join('-')}`)
}
}
</script>
<style lang="scss">
.compare-selection {
@apply w-full lg:w-[64.8rem] lg:h-[53.2rem] bg-white rounded-t-[0.6rem] lg:rounded-[0.6rem] flex flex-col lg:pb-[4.9rem];
&>div:nth-of-type(1) {
@apply flex justify-between items-center gap-3 p-6 lg:p-8 pb-6 border lg:border-b-2 border-[#E5E5E5];
h2 {
@apply text-zinc-800 text-sm lg:text-xl font-bold font-vazir;
}
button {
@apply -m-3;
.p-button-icon {
@apply text-[1.4em] lg:text-3xl text-[#B2B2B2];
}
}
}
&>div:nth-of-type(2) {
@apply w-full flex flex-col p-6 pt-8 lg:p-10 lg:pb-0 gap-6;
&>div:nth-of-type(1) {
@apply w-full h-[3.4rem] 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-xs lg:text-lg font-vazir;
}
}
}
&>div:nth-of-type(2) {
@apply w-full flex justify-between items-center;
span {
@apply text-[#7F7F7F] text-xs lg:text-base font-medium font-vazir;
}
}
&>div:nth-of-type(3) {
@apply overflow-y-auto h-[23.8rem] lg:h-[34.4rem] w-full;
&>ul {
@apply w-full grid lg:grid-cols-2 lg:gap-6 h-max;
li {
@apply cursor-pointer;
a {
@apply pointer-events-none;
}
.product-card {
@apply lg:w-[28.8rem] lg:h-[32.3rem];
}
@media (max-width: 1024px) {
.product-card {
@apply w-full 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-4;
.p-button-icon {
@apply text-xl;
}
}
}
}
}
}
}
}
}
}
</style>
+65
View File
@@ -0,0 +1,65 @@
<template>
<div class="compare-product">
<template v-if="ids.length > 1">
<Button icon="isax isax-close-circle5" severity="secondary" text rounded @click="remove" />
</template>
<img :src="coverImage" :alt="title">
<h2>{{ title }}</h2>
<span>{{ numberFormat(price) }} <small>{{ $t('priceUnit') }}</small></span>
<Button :label="$t('addToCart')" @click="handleClick" />
</div>
</template>
<script setup>
const props = defineProps(['data'])
const { coverImage, title, price, _id: id } = props.data
const route = useRoute()
const router = useRouter()
const ids = route.params.ids.split('-')
const remove = () => {
const temp = ids.filter((item) => item != id)
router.push('/compare/' + temp.join('-'))
}
const handleClick = () => { }
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.compare-product {
@apply relative flex flex-col items-center gap-4 w-[10.7rem] h-[16.6rem] lg:w-[28.1rem] lg:h-[25.6rem] pb-[1.7rem];
&>button.p-button-icon-only {
@apply absolute left-0 -top-4 p-0 text-[1.4em] lg:text-[1.6em] #{!important};
}
img {
@apply w-20 h-20 lg:w-[12.5rem] lg:h-[12.5rem];
}
&>h2 {
@apply w-[8.4rem] lg:w-[18.8rem] text-center text-zinc-800 text-sm lg:text-lg font-medium font-vazir mt-2.5 mb-0.5;
@apply truncate;
}
&>span {
@apply text-zinc-800 text-sm lg:text-lg font-medium font-vazir;
small {
@apply text-zinc-800 text-[0.7em] lg:text-[0.8em] font-medium font-vazir;
}
}
&>button:last-child {
@apply w-[6.4rem] h-[2.1rem] p-0 lg:w-[9.8rem] lg:h-12 shrink-0;
.p-button-label {
@apply text-white text-xs lg:text-base font-bold font-iran-sans;
}
}
}
</style>
+57
View File
@@ -0,0 +1,57 @@
<template>
<section class="compare-product-list">
<ul>
<template v-for="(product, i) in items" :key="i">
<li v-if="i > 0">
<span></span>
</li>
<li>
<CompareProductCard :data="product" />
</li>
</template>
<li v-if="ids.length < (device == 'mobile' ? 2 : 3)">
<Button :label="$t('selectProduct')" outlined @click="handleCLick" />
</li>
</ul>
</section>
</template>
<script setup>
const { data: items } = inject('data')
const { device, dialog } = inject('service')
const route = useRoute()
const ids = route.params.ids.split('-')
const handleCLick = () => {
dialog.show(resolveComponent('CompareDialogSelection'))
}
</script>
<style lang="scss">
.compare-product-list {
@apply w-full max-w-[112.3rem] mx-auto pt-[3.3rem] lg:pt-[4.4rem];
ul {
@apply flex gap-3 lg:border-b justify-center lg:justify-start;
li {
&:last-child:has(.p-button-outlined) {
@apply flex justify-center items-center w-[10.7rem] h-[16.6rem] lg:w-[28.1rem] lg:h-[25.6rem];
button {
@apply w-[6.2rem] h-[2.6rem] lg:w-[8.1rem] lg:h-[3.3rem];
span {
@apply text-xs font-bold font-vazir lg:text-base lg:font-iran-sans
}
}
}
&:has(>span) {
@apply self-stretch w-px bg-neutral-200;
}
}
}
}
</style>
+42
View File
@@ -0,0 +1,42 @@
<template>
<div class="compare-spec-item">
<h3>{{ title }}</h3>
<ul>
<li v-for="item in items" :key="item">
<span>{{ item }}</span>
</li>
</ul>
</div>
</template>
<script setup>
const props = defineProps(['data'])
const { title , items } = props.data
</script>
<style lang="scss">
.compare-spec-item {
@apply flex flex-col gap-[1.1rem];
h3 {
@apply text-[#999999] text-xs lg:text-base font-normal font-vazir text-center lg:text-right;
}
ul {
@apply flex;
li {
@apply w-full lg:w-[28.1rem] text-center;
span {
@apply text-[#333333] text-xs font-normal font-vazir lg:text-base lg:font-medium;
}
&:last-child{
// @apply max-lg:text-left;
}
}
}
}
</style>
+32
View File
@@ -0,0 +1,32 @@
<template>
<section class="compare-specs">
<h2>{{ $t('specifications') }}</h2>
<ul>
<li v-for="(items, title) in sortedSpecs" :key="title">
<CompareSpecItem :data="{ items, title }" />
</li>
</ul>
</section>
</template>
<script setup>
const { sortedSpecs } = inject('data')
</script>
<style lang="scss">
.compare-specs {
@apply mt-[1.9rem] container lg:w-[107.6rem] flex flex-col gap-8 mx-auto mb-[5.6rem];
&>h2 {
@apply text-zinc-800 text-sm lg:text-base font-medium font-vazir
}
&>ul {
@apply flex flex-col gap-6;
&>li {
@apply h-[5.1rem] lg:h-[5.1rem] border-b border-neutral-200;
}
}
}
</style>
+82
View File
@@ -0,0 +1,82 @@
<template>
<section class="amazing-offers">
<div>
<aside>
<h2>
<span>{{ $t("amazingOffers") }}</span>
<span>{{ $t("asanMarket") }}</span>
</h2>
<img src="/images/amazing.svg" />
</aside>
<Swiper :slides-per-view="'auto'">
<SwiperSlide v-for="(product, i) in amazingOffers" :key="i">
<ProductCard :data="product" type="main" />
</SwiperSlide>
</Swiper>
</div>
</section>
</template>
<script setup>
const { amazingOffers } = inject('data')
</script>
<style lang="scss">
.amazing-offers {
@apply w-full flex justify-center;
@apply mt-[2.6rem] lg:mt-[11.3rem];
&>div {
@apply max-w-full w-full flex shrink-0 bg-[#F2F2F2] overflow-hidden;
@apply gap-2.5 pr-2.5 h-max;
@apply lg:container lg:rounded-2xl lg:pr-[2.6rem] lg:gap-[2.4rem];
aside {
@apply bg-[linear-gradient(to_bottom,_#47B556_0%,_#F2F2F200_60%)];
@apply flex flex-col items-center justify-between;
@apply relative rounded-[0.6rem] shrink-0 mt-2;
@apply w-[6.2rem] h-[11.5rem] pt-4 px-2.5;
@apply lg:w-[11.6rem] lg:h-[20.1rem] lg:my-7 lg:px-5;
&::before {
@apply content-['_'] absolute inset-[0.2rem] bg-[#F2F2F2] rounded-lg;
}
h2 {
@apply flex flex-col items-center justify-center z-0 gap-2;
span {
@apply text-center first:text-[#163D22] last:text-primary-600;
@apply font-vazir font-black text-base lg:text-3xl;
}
}
img {
@apply w-[3.4rem] h-[4.3rem] lg:w-[9.6rem] lg:h-[12rem] self-start lg:-mb-20 z-0;
}
}
.swiper {
@apply w-full h-full rounded-xl relative rtl lg:ltr lg:px-6;
.swiper-wrapper {
@apply mt-2 mb-3 lg:my-6 z-0;
.swiper-slide {
@apply h-max w-max ml-[0.3rem] lg:ml-6 first:ml-0 rtl;
}
}
&::before {
@apply content-['_'] w-6 absolute inset-y-0 -right-6 z-10;
@apply shadow-[-5px_0px_23px_0px_#00000040] max-lg:hidden;
}
&::after {
@apply content-['_'] w-6 absolute inset-y-0 -left-6 z-10;
@apply shadow-[5px_0px_23px_0px_#00000040] max-lg:hidden;
}
}
}
}
</style>
+21
View File
@@ -0,0 +1,21 @@
<template>
<section class="asan-service">
<a href="http://jobs.asan-service.com/" target="_blank">
<img src="/images/asan-service.svg" alt="" />
</a>
</section>
</template>
<script setup></script>
<style lang="scss">
.asan-service {
@apply w-full overflow-hidden max-lg:hidden;
a {
img {
@apply mx-auto xl:h-[20.6rem] object-contain object-center;
}
}
}
</style>
+29
View File
@@ -0,0 +1,29 @@
<template>
<section class="home-banners">
<div>
<router-link v-for="({link, image}, i) in banners" :key="i" :to="link">
<img :src="image" />
</router-link>
</div>
</section>
</template>
<script setup>
const { banners } = inject('data')
</script>
<style lang="scss">
.home-banners {
@apply w-full flex justify-center mt-6 lg:mt-[6.5rem];
div {
@apply flex flex-wrap justify-center gap-3 lg:gap-[1.4rem] ;
a {
img {
@apply object-contain w-[21.3rem] h-[6.6rem] lg:w-[47.9rem] lg:h-[18.8rem];
}
}
}
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template>
<section class="est-products">
<div>
<div v-for="(items, key) in { newProduct, topProduct }" :key="key">
<h2>{{ $t(key) }}</h2>
<h3>
<span>
{{ $t("toViewProducts", { title: $t(key) }) }}
</span>
<router-link to="/products">
{{ $t("clickHere") }}
</router-link>
</h3>
<div>
<template v-for="(btn, i) in btns.arrows" :key="i">
<Button v-if="device == 'desktop'" v-bind="btn" />
</template>
<Swiper v-bind="swipers[key]" :class="key">
<SwiperSlide v-for="(item, i) in items" :key="i">
<ProductCard :data="item" type="main" />
</SwiperSlide>
</Swiper>
</div>
</div>
<HomeMostPopularThisWeek />
</div>
</section>
</template>
<script setup>
const { newProduct, topProduct } = inject('data')
const { swipers, btns } = inject('init')
const { device } = inject('service')
</script>
<style lang="scss">
.est-products {
@apply w-full bg-[#F6F8FA] py-6 lg:py-20 overflow-hidden;
&>div {
@apply w-full lg:container flex flex-col mx-auto;
&>div:not([class]) {
@apply flex flex-col;
&:nth-of-type(2) {
@apply order-last mt-10 lg:mt-20;
}
&>h2 {
@apply text-[#0B1F11] font-bold font-vazir mr-6 lg:mr-4;
@apply text-lg lg:text-3xl;
}
&>h3 {
@apply flex items-center gap-1.5 mt-1.5 mr-6 font-vazir;
@apply text-sm lg:text-xl lg:mt-2 lg:mr-4;
span {
@apply text-[#8C8C8C] lg:leading-9;
}
a {
@apply text-primary-600 font-bold lg:leading-[2.8rem];
}
}
&>div {
@apply flex gap-10 w-full h-max mt-2 mb-8 lg:my-16 relative;
.swiper {
@apply w-full h-max mx-0 p-2.5 lg:py-4;
.swiper-wrapper {
@apply w-max h-max;
.swiper-slide {
@apply h-max w-max rtl ml-3 lg:ml-5 last:ml-0;
}
}
}
&>button {
@apply w-10 h-10 self-center absolute text-xl p-0 bg-[#B2B2B2] border-none #{!important};
@apply first-of-type:-right-16 last-of-type:-left-16 max-lg:hidden;
}
}
}
}
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<section class="latest-posts">
<div>
<h2>{{ $t('mediaAndContent') }}</h2>
<h3>
<span>{{ $t('toViewLatestMediaAndContent') }}</span>
<router-link to="/blog">{{ $t('clickHere') }}</router-link>
</h3>
<div>
<ul>
<li v-for="(post, i) in lastBlog" :key="i">
<BlogCard :data="post" />
</li>
</ul>
</div>
</div>
</section>
</template>
<script setup>
const { lastBlog } = inject('data')
</script>
<style lang="scss">
.latest-posts {
@apply w-full flex justify-center mt-[5.3rem] lg:mt-[8.8rem];
&>div {
@apply container flex flex-col w-full;
&>h2 {
@apply text-[#0B1F11] font-bold font-vazir mr-6 lg:mr-4;
@apply text-lg lg:text-3xl;
}
&>h3 {
@apply flex items-center gap-1.5 mt-1.5 mr-6 font-vazir;
@apply text-sm lg:text-xl lg:mt-2 lg:mr-4;
span {
@apply text-[#8C8C8C] leading-9;
}
a {
@apply text-primary-600 font-bold leading-[2.8rem];
}
}
&>div {
@apply w-full flex lg:overflow-x-auto;
&::-webkit-scrollbar {
@apply hidden;
}
&>ul {
@apply flex flex-wrap lg:flex-nowrap justify-center gap-5 mx-2 lg:w-max h-max mt-4 lg:my-12 self-center;
}
}
}
}
</style>
+59
View File
@@ -0,0 +1,59 @@
<template>
<section class="main-categories">
<Swiper slides-per-view="auto">
<SwiperSlide v-for="(item, i) in store.items" :key="i">
<router-link :to="{ path: '/search', query: { category: item.link } }">
<img :src="item.headerImage" />
<h3>{{ $t(item.name) }}</h3>
</router-link>
</SwiperSlide>
</Swiper>
</section>
</template>
<script setup>
const store = useCategoriesStore();
</script>
<style lang="scss">
.main-categories {
@apply w-full flex mt-12 lg:mt-[9.6rem];
.swiper {
@apply container py-2 lg:py-4 max-lg:px-4;
.swiper-wrapper {
@apply w-max mx-auto;
.swiper-slide {
@apply w-max ml-2.5 lg:ml-[1.4rem] last:ml-0;
a {
@apply flex flex-col items-center shrink-0;
@apply w-[6.3rem] gap-2.5 pb-4;
@apply lg:w-[10.8rem] lg:gap-5 lg:pb-6;
// &::before {
// @apply content-['_'] h-[5.6rem] lg:h-[10.8rem] rounded-2xl absolute bottom-0 inset-x-0 -z-10;
// @apply shadow-[3px_2px_10px_#87C29A] lg:shadow-[5px_5px_19px_0px_#87C29A,_-5px_-1px_15px_0px_#ffffffcc];
// background: linear-gradient(
// 133deg,
// #e9ecef 0%,
// white 100%
// );
// }
img {
@apply w-[4.4rem] h-[4.4rem] rounded-full lg:w-[9.1rem] lg:h-32;
}
h3 {
@apply text-[#191919] font-medium font-vazir;
@apply text-[0.625em] lg:text-[1.1875em];
}
}
}
}
}
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<template>
<div class="most-popular-this-week">
<h2>{{ $t("mostPopularsThisWeek") }}</h2>
<router-link to="/">
<span>{{ $t("show") }}</span>
<i class="isax isax-arrow-left" />
</router-link>
<ul>
<li v-for="(brand, i) in topBrand" :key="i">
<router-link :to="{ path: '/search', query: { brand: brand.link } }">
<img :src="brand.logo" :alt="brand.title" />
</router-link>
</li>
</ul>
</div>
</template>
<script setup>
const { topBrand } = inject('data')
</script>
<style lang="scss">
.most-popular-this-week {
background: linear-gradient(270deg, #6ea89a 0%, #597c74 100%);
@apply container flex flex-wrap-reverse items-center rounded-lg;
@apply w-auto p-3 gap-1.5 mx-6 max-w-[97.1rem] min-h-[5.6rem];
@apply min-[445px]:justify-center sm:flex-row sm:mx-2 sm:gap-4;
@apply lg:h-[7.6rem] lg:px-7 lg:gap-[1.6rem] xl:px-[4.4rem];
h2 {
@apply text-white font-bold font-vazir whitespace-nowrap;
@apply text-xl lg:text-3xl;
}
&>a {
@apply w-[5.1rem] h-7 flex justify-center items-center gap-1.5 rounded bg-white px-2.5 py-[0.3rem] order-first;
@apply lg:w-32 lg:h-[2.6rem] lg:gap-3 lg:px-[0.9rem] text-[#549887] min-[445px]:order-none;
span {
@apply text-[0.7em] lg:text-[1.2em] font-medium font-iran-sans;
}
i {
@apply text-lg lg:text-2xl;
}
}
ul {
@apply flex gap-[1.4rem] lg:gap-[2.6rem] items-center mb-1.5 sm:mb-0 md:mr-auto;
li {
@apply h-max py-px;
img {
@apply object-contain w-14 h-6 lg:w-12 lg:h-12;
}
}
}
}
</style>
+83
View File
@@ -0,0 +1,83 @@
<template>
<section class="popular-categories">
<div>
<h2>{{ $t("popularCategories") }}</h2>
<div>
<ul>
<li v-for="({ link, headerImage, name }, i) in topCategory" :key="i">
<router-link :to="{ path: '/search', query: { category: link } }">
<div>
<!-- <span :style="style(bg)"></span> -->
<img :src="headerImage" />
</div>
<h3>{{ name }}</h3>
</router-link>
</li>
</ul>
</div>
</div>
</section>
</template>
<script setup>
const { topCategory } = inject('data')
const bg = reactive({ from: '#E1FFEB', to: '#D8E7FA' })
const style = (bg) => `background: linear-gradient(143.73deg, ${bg.from} 5.62%, ${bg.to} 99.35%)`
</script>
<style lang="scss">
.popular-categories {
@apply w-full flex justify-center mt-16 lg:mt-[10.8rem];
&>div {
@apply container flex flex-col items-center gap-10 lg:gap-16 w-full;
h2 {
@apply text-[#0B1F11] text-xl lg:text-3xl font-bold font-vazir;
}
&>div {
@apply overflow-x-auto w-full rtl;
&::-webkit-scrollbar {
@apply hidden;
}
ul {
@apply rtl grid grid-rows-2 grid-flow-col gap-[1.9rem] w-max mx-auto lg:gap-0 lg:gap-y-8;
li {
a {
@apply m-auto flex flex-col items-center;
@apply w-[4.6rem] h-[6rem] justify-between;
@apply lg:w-[16.2rem] lg:h-[15.6rem] lg:gap-[0.6rem];
div {
@apply flex relative h-full w-full xl:pt-2.5;
// span {
// @apply rounded-full -z-10;
// @apply w-[4.3rem] h-[4.3rem];
// @apply lg:w-[11.8rem] lg:h-[11.8rem];
// }
img {
@apply m-auto object-contain absolute left-1/2 -translate-x-1/2;
@apply w-[3.6rem] h-[3rem];
@apply lg:w-[11.8rem] lg:h-[11.8rem];
}
}
h3 {
@apply text-[#191919] font-bold font-vazir;
@apply text-[0.7em] lg:text-[1.2em] truncate w-full text-center;
}
}
}
}
}
}
}
</style>
+40
View File
@@ -0,0 +1,40 @@
<template>
<section class="producers">
<h2>{{ title }}</h2>
<ul>
<li v-for="({ link, logo, title }, i) in creatorBrand" :key="i">
<router-link :to="{ path: '/search', query: { brand: link } }">
<img :src="logo" :alt="title" />
</router-link>
</li>
</ul>
</section>
</template>
<script setup>
defineProps(['title'])
const { creatorBrand } = inject('data')
</script>
<style lang="scss">
.producers {
@apply w-full min-h-[18.9rem] bg-[#FAFAFA] flex flex-col items-center gap-12 py-12 mt-16 max-lg:hidden;
h2 {
@apply text-primary-600 text-3xl font-bold font-vazir;
}
ul {
@apply w-full flex flex-wrap items-center justify-center gap-12;
li {
@apply h-[2.8rem] w-11;
img {
@apply w-full h-full object-contain object-center;
}
}
}
}
</style>
+58
View File
@@ -0,0 +1,58 @@
<template>
<section class="reviews">
<Swiper slides-per-view="auto">
<SwiperSlide v-for="(review, i) in reviews" :key="i">
<router-link :to="'blog/' + review.link">
<img :src="review.coverPath" />
<h2>{{ review.title }}</h2>
<i class="isax isax-arrow-circle-left5" />
</router-link>
</SwiperSlide>
</Swiper>
</section>
</template>
<script setup>
const { reviews } = inject('data')
</script>
<style lang="scss">
.reviews {
@apply w-full flex justify-center my-9 lg:my-[4.4rem];
.swiper {
@apply container py-2 lg:py-4 rtl max-lg:px-4;
.swiper-wrapper {
@apply w-max;
.swiper-slide {
@apply w-min ml-2.5 lg:ml-6 last:ml-0;
a {
@apply flex items-end justify-between relative rounded-lg overflow-hidden text-white z-0;
@apply w-[9.9rem] h-[7.6rem] gap-4 px-[0.3rem] pb-2.5;
@apply lg:w-[18.3rem] lg:h-56 lg:gap-[1.3rem] lg:p-6;
img {
@apply absolute inset-0 z-0;
}
&::after {
background: linear-gradient(180deg, #0000 43.02%, #00000096 100%);
@apply content-['_'] absolute inset-0 z-[1];
}
h2 {
@apply text-xs lg:text-[1.3em] lg:leading-8 font-black font-iran-sans z-[2] whitespace-break-spaces;
}
i {
@apply text-lg lg:text-2xl z-[2] -mb-2 lg:-mb-1;
}
}
}
}
}
}
</style>
+51
View File
@@ -0,0 +1,51 @@
<template>
<section class="top-slider">
<template v-for="(btn, i) in btns.arrows" :key="i">
<Button v-if="device == 'desktop'" v-bind="btn" />
</template>
<Swiper v-bind="swipers.topSlider">
<SwiperSlide v-for="{ link, images, title, id } in sliders" :key="id">
<router-link :to="link">
<img :title="title" :src="images[device]" />
</router-link>
</SwiperSlide>
</Swiper>
</section>
</template>
<script setup>
const { swipers, btns } = inject('init')
const { device } = inject('service')
const { sliders } = inject('data')
</script>
<style lang="scss">
.top-slider {
@apply w-full h-max flex z-0;
.swiper {
@apply w-full h-max;
.swiper-wrapper {
@apply z-0 h-max bg-gray-50;
.swiper-slide {
@apply w-screen h-full;
a {
img {
@apply w-full object-cover h-full;
}
}
}
}
}
&>button {
@apply self-center absolute p-0 bg-white/40 border-none max-lg:hidden z-10 #{!important};
@apply lg:w-8 lg:h-8 lg:text-base lg:first-of-type:right-8 lg:last-of-type:left-9 #{!important};
@apply xl:w-9 xl:h-9 xl:text-lg xl:first-of-type:right-12 xl:last-of-type:left-12 #{!important};
@apply 2xl:w-10 2xl:h-10 2xl:text-xl 2xl:first-of-type:right-16 2xl:last-of-type:left-16 #{!important};
}
}
</style>
+35
View File
@@ -0,0 +1,35 @@
<template>
<div class="become-seller">
<p>
{{ $t('becomeSeller') }}!
<br />
{{ $t('sellingInAsanMarket') }}
</p>
<Button :label="$t('becomeSeller')" rasiad text @click="$router.push('/panel/seller/become')" />
</div>
</template>
<script setup>
</script>
<style lang="scss">
.become-seller {
@apply w-[21.4rem] h-[29.4rem] p-6 flex flex-col justify-between rounded-[0.6rem];
@apply bg-no-repeat bg-cover bg-[url('/images/sell.svg')];
@apply lg:w-[24.2rem] lg:h-[34.9rem] lg:p-8;
p {
@apply text-right text-white text-xl lg:text-2xl font-bold font-vazir;
}
button {
@apply w-[18.4rem] h-12 lg:w-[20.2rem] bg-white rounded-[0.6rem] mx-auto border-none #{!important};
.p-button-label {
@apply text-[#47B556] text-xs lg:text-sm font-medium font-vazir;
}
}
}
</style>
+93
View File
@@ -0,0 +1,93 @@
<template>
<div class="comment-item">
<img :src="product.coverImage" />
<div>
<div>
<div>
<h2>{{ title }}</h2>
<span>{{ (new Date(createdAt)).toLocaleDateString('fa-IR') }}</span>
</div>
<span>{{ statuses[confirm] }}</span>
<Button v-bind="btns.options.props" @click="showOptions" />
</div>
<p>{{ content }}</p>
</div>
</div>
</template>
<script setup>
const props = defineProps(['data'])
const { id, product, title, content, createdAt, confirm } = props.data
const { statuses, btns, options } = inject('init')
const { device, dialog, menu } = inject('service')
const showOptions = (e) => {
if (device.value == 'desktop') {
menu.items = options.comment;
menu.ref.toggle(e)
} else if (device.value == 'mobile')
dialog.show(resolveComponent(''), { id })
}
</script>
<style lang="scss">
.comment-item {
@apply w-[21.4rem] flex gap-[1.1rem] lg:gap-[1.9rem] lg:w-full lg:h-20;
img {
@apply w-[3.8rem] h-[3.8rem] lg:w-[5rem] lg:h-[5rem];
}
&>div {
@apply flex flex-col gap-3 lg:w-full;
&>div {
@apply h-20 flex justify-between items-center pb-3 border-b border-[#F0F0F1] gap-[1.9rem];
&>div {
@apply flex flex-col gap-1.5 font-vazir;
h3 {
@apply flex items-center gap-2 text-[#019907] lg:hidden;
i {
@apply text-base;
}
small {
@apply text-[0.6em];
}
}
h2 {
@apply text-[#333333] text-sm lg:text-base font-bold;
}
span {
@apply text-neutral-400 text-[0.7em] lg:text-sm font-medium;
}
}
&>span {
@apply text-[#019907] text-xs lg:text-base font-vazir mr-auto;
}
&>button {
@apply h-8 w-8 -ml-3 #{!important};
.p-button-icon {
@apply text-[#333333] text-lg;
}
}
}
&>p {
@apply w-[16.5rem] text-[#4C4C4C] text-xs leading-5 font-vazir lg:w-full;
}
}
}
</style>
+95
View File
@@ -0,0 +1,95 @@
<template>
<div class="factor-filters">
<div v-if="device == 'mobile'">
<Button v-bind="btns.close.props" @click="dialog.close()" />
<h2>{{ $t("sortBy") }}</h2>
</div>
<ul>
<li v-for="(s, i) in switches" :key="i">
<label :for="s">
{{ $t("searchBy" + s[0].toUpperCase() + s.slice(1)) }}
</label>
<InputSwitch v-model="filters[s]" :value="s" :inputId="s" />
</li>
<li v-for="(r, i) in radios" :key="i">
<label :for="r">{{ $t(r) }}</label>
<RadioButton v-model="filters[sort]" :value="r" :inputId="r" />
</li>
</ul>
</div>
</template>
<script setup>
import init from '../../initialize/panel.js'
const { btns, radios, switches } = init();
const { device } = inject('service')
const dialog = inject('dialogRef')
const filters = defineModel({ default: {} });
</script>
<style lang="scss">
.factor-filters {
@apply lg:w-[17.8rem] lg:h-[19.1rem] bg-white rounded-t-2xl lg:rounded-2xl lg:border p-6 lg:p-8;
@apply flex flex-col gap-9 rtl;
&>div {
@apply flex items-center gap-4;
&>button {
@apply -mx-5 -my-3 text-[#333333] text-xl #{!important};
}
&>h2 {
@apply text-[#333333] text-sm font-medium font-vazir;
}
}
ul {
@apply flex flex-col gap-[1.1rem] lg:gap-4;
li {
@apply w-full flex justify-between items-center;
&:nth-child(2) {
@apply border-b pb-4;
}
label {
@apply text-[#333333] text-xs lg:text-[1.1em] font-medium font-vazir cursor-pointer;
}
.p-inputswitch {
@apply w-8 h-[1.1rem] lg:w-6 lg:h-4;
.p-inputswitch-slider {
@apply bg-[#A6A6A6];
@apply w-8 h-[1.1rem] lg:w-6 lg:h-4 hover:bg-[#929191] border-0 #{!important};
&::before {
@apply w-3 h-3 left-1 lg:w-2.5 lg:h-2.5 lg:left-0.5 mt-0 top-1/2 -translate-y-1/2 bg-white;
}
}
}
.p-inputswitch-checked .p-inputswitch-slider {
@apply bg-primary-600 hover:bg-primary-600 #{!important};
&::before {
@apply translate-x-3 lg:translate-x-2;
}
}
.p-radiobutton-checked {
.p-radiobutton-box {
@apply bg-white border-primary-600 border hover:bg-white #{!important};
.p-radiobutton-icon {
@apply w-2.5 h-2.5 bg-primary-600;
}
}
}
}
}
}
</style>
+81
View File
@@ -0,0 +1,81 @@
<template>
<div class="information">
<h2>{{ $t('userInformation') }}</h2>
<div>
<ul>
<li v-for="(value, key) in data" :key="key">
<label>{{ $t(key) }} : </label>
<span>{{ value }}</span>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import init from '../../initialize/userProfileInformation'
const { items } = init()
const profile = inject('data')
const data = reactive({});
const temp = {
true: 'تایید شده', false: 'تایید نشده', '0': 'مرد', '1': 'خانم'
}
items.forEach(key => {
const value = profile[key];
data[key] = temp[value] || value
});
</script>
<style lang="scss">
.information {
@apply w-[21.4rem] lg:w-[49.8rem] lg:h-[34.9rem];
@apply lg:bg-[#FAFAFA] rounded-[0.6rem] lg:border-2 lg:border-[#E5E5E5];
@apply flex flex-col gap-6 lg:p-8 lg:pl-2.5;
h2 {
@apply max-lg:hidden text-[#333333] text-2xl font-bold font-iran-sans flex items-center gap-3.5;
&::before {
@apply content-['_'] h-6 w-1.5 rounded-full mb-1 bg-primary-600;
}
}
div {
@apply w-full h-full lg:h-[27.1rem] lg:overflow-y-auto;
&::-webkit-scrollbar {
@apply w-[0.2rem] lg:w-1.5 bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply bg-[#E5E5E5];
}
&::-webkit-scrollbar-track {
@apply bg-transparent;
}
ul {
@apply flex flex-col gap-y-3 gap-x-6 pl-4 lg:pl-3 h-max lg:h-[26.3rem] lg:flex-wrap;
li {
@apply w-[21.4rem] lg:w-[22.1rem] h-[2.8rem] lg:h-12;
@apply bg-[#F2F2F2] rounded-lg px-4 flex items-center gap-2 font-vazir;
@apply last:py-2 last:items-start;
label {
@apply text-[#7F7F7F] text-xs lg:text-base whitespace-nowrap leading-6;
}
span {
@apply text-[#333333] text-xs lg:text-[1.1em] font-medium leading-6;
}
}
}
}
}
</style>
+85
View File
@@ -0,0 +1,85 @@
<template>
<div class="national-card">
<h2>{{ $t("nationalCard") }}</h2>
<p>{{ $t("nationalCardDesc") }}</p>
<img :src="nationalCard" />
<label>
<i :class="tags[confirmToSales].icon"></i>
<span>{{ tags[confirmToSales].label }}</span>
</label>
<div>
<span>{{ nationalCode }}</span>
<i class="isax isax-link-21" />
</div>
</div>
</template>
<script setup>
const { nationalCard, confirmToSales, nationalCode } = inject('data')
const tags = reactive({
true: {
icon: 'isax isax-tick-circle',
label: 'تاییده شده'
},
false: {
icon: 'isax isax-close-circle',
label: 'تاییده نشده'
}
})
</script>
<style lang="scss">
.national-card {
@apply w-[21.4rem] h-[28.4rem] lg:w-[24.2rem] lg:h-[34.9rem];
@apply flex flex-col gap-4 p-6 lg:p-8 bg-neutral-50 rounded-[0.6rem] border-neutral-200;
@apply border lg:border-2;
h2 {
@apply text-[#333333] text-lg lg:text-2xl font-bold font-iran-sans;
@apply flex items-center gap-4 lg:gap-3.5;
&::before {
@apply content-['_'] w-[0.2rem] h-[1.1rem] lg:h-6 lg:w-1.5 lg:mb-1 rounded-full bg-primary-600;
}
}
p {
@apply text-[#7F7F7F] text-xs lg:text-base font-medium font-vazir;
}
img {
@apply w-[18.4rem] h-[11.6rem] lg:w-[20.2rem] lg:h-[12.8rem] bg-[#F2F2F2] rounded-[0.6rem] overflow-hidden relative;
@apply object-cover object-center;
}
label {
@apply flex items-center gap-2 text-[#7F7F7F];
span {
@apply text-xs font-iran-sans;
}
i {
@apply text-xl;
}
}
div {
@apply h-[2.7rem] lg:h-[3.1rem] flex justify-end items-center gap-2 px-4;
@apply text-[#333333] bg-[#F2F2F2] rounded-lg shrink-0 mt-auto;
i {
@apply text-2xl;
}
span {
@apply font-iran-sans text-xs lg:text-base;
}
}
}
</style>
+51
View File
@@ -0,0 +1,51 @@
<template>
<div class="notification-item" :class="{ 'unread': !read }">
<h2>{{ title }} </h2>
<div>
<span>{{ new Date(createdAt).toLocaleTimeString('fa-IR') }}</span>
<i />
<span>{{ new Date(createdAt).toLocaleDateString('fa-IR') }}</span>
</div>
<p v-html="content"></p>
</div>
</template>
<script setup>
const props = defineProps(['data'])
const { title, content, createdAt, read } = props.data
</script>
<style lang="scss" scoped>
.notification-item {
@apply w-full h-[5.8rem] lg:h-[6.4rem] font-vazir rounded-[0.6rem] bg-[#FAFAFA];
@apply flex flex-wrap items-center justify-between gap-y-2 p-6 lg:gap-2.5;
h2 {
@apply text-[#333333] text-xs lg:text-base lg:font-bold lg:w-full truncate;
}
p {
@apply text-neutral-600 text-xs lg:text-base leading-loose truncate w-full lg:w-max;
}
div {
@apply flex items-center gap-1.5 lg:gap-3 lg:order-last;
span {
@apply text-zinc-500 text-[0.6em] lg:text-base font-medium whitespace-nowrap;
}
i {
@apply w-2 h-2 bg-neutral-200 rounded-full;
}
}
&.unread {
@apply shadow-[0px_0px_7px_#0000001a] bg-white;
h2 {
@apply font-bold;
}
}
}
</style>
+100
View File
@@ -0,0 +1,100 @@
<template>
<div class="order-item">
<div>
<ul>
<li v-for="(value, key) in items" :key="key">
<label>{{ $t(key) }}:</label>
<span>{{ value }}</span>
</li>
</ul>
<router-link :to="`/panel/factors?id=${id}`">
<Button :label="$t('showFactor')" size="small" />
</router-link>
</div>
<Swiper :slides-per-view="'auto'" :space-between="16">
<SwiperSlide v-for="(item, i) in products" :key="i">
<ProductCard :data="item.product" type="minimal" />
</SwiperSlide>
</Swiper>
</div>
</template>
<script setup>
const props = defineProps(['data'])
const { _id: id, totalAmount, status, orderDate, products } = props.data
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
const { t } = useI18n()
const items = reactive({
documentNumber: id,
amount: numberFormat(totalAmount) + ' ' + t('priceUnit'),
orderStatus: t('orderStatuses.' + status.toLowerCase()),
orderDate: new Date(orderDate).toLocaleString('fa-IR')
})
</script>
<style lang="scss">
.order-item {
@apply w-[21.4rem] h-[29.1rem] lg:w-full lg:h-[19.3rem] overflow-hidden;
@apply flex flex-col-reverse lg:flex-row bg-white border-2 border-[#E5E5E5];
@apply rounded-[0.6rem] max-lg:rounded-t-xl;
&>div:first-of-type {
@apply w-full h-[15.1rem] lg:w-[29.6rem] lg:h-full max-lg:border-t-2 border-[#E5E5E5];
@apply flex flex-col justify-between whitespace-nowrap;
@apply gap-[1.9rem] p-6 lg:gap-10 lg:p-8;
ul {
@apply flex flex-col gap-4;
li {
@apply flex gap-3 items-center font-vazir text-xs lg:text-lg;
label {
@apply text-zinc-500;
}
span {
@apply text-zinc-800 font-medium;
}
&:last-child span {
@apply ltr;
}
}
}
button {
@apply w-full h-10 lg:w-[9.3rem] rounded-lg #{!important};
.p-button-panel {
@apply font-medium font-vazir text-xs lg:text-base;
}
}
}
.swiper {
@apply w-full max-w-[67.6rem] h-56 lg:h-full relative ltr px-[0.9rem];
.swiper-wrapper {
@apply items-center z-0;
.swiper-slide {
@apply h-max w-max;
}
}
&::before {
@apply shadow-[-5px_0px_23px_0px_#00000040];
@apply content-['_'] absolute inset-y-0 w-6 -right-8 lg:-right-6 opacity-50 z-10;
}
&::after {
@apply shadow-[5px_0px_23px_0px_#00000040];
@apply content-['_'] absolute inset-y-0 w-6 -left-8 lg:-left-6 opacity-50 z-10;
}
}
}
</style>
+65
View File
@@ -0,0 +1,65 @@
<template>
<ul class="profile-menu">
<li v-for="(item, i) in menu.profile" :key="i">
<template v-if="i != 2 || user?.profile?.confirmToSales">
<component :is="item.link ? 'router-link': 'div'" :to="'/panel/' + item.url" @click="handleClick(item.key)"
v-ripple>
<i :class="item.icon"></i>
<span>{{ item.label }}</span>
<i v-if="item.key != 'logout'" class="isax isax-arrow-left-2" />
</component>
</template>
</li>
</ul>
</template>
<script setup>
const { menu } = inject('init');
const { dialog } = inject('service')
const handleClick = (key) => {
if(key == 'logout')
dialog.show(resolveComponent('PanelDialogLogout'))
else if(key == 'password')
dialog.show(resolveComponent('PanelDialogPasswordChange'))
}
</script>
<style lang="scss">
.profile-menu {
@apply flex flex-col gap-1.5 md:order-first;
li {
a,
div {
@apply w-[21.4rem] h-[3.1rem] bg-[#FAFAFA] rounded-[0.6rem] overflow-hidden relative;
@apply flex items-center gap-3 px-[1.1rem] text-[#7F7F7F];
i:first-child {
@apply text-[1.4em];
}
span {
@apply text-sm font-vazir ml-auto;
}
i:last-child {
@apply text-base;
}
}
&:last-child a {
i:first-child,
span {
@apply text-[#AD3434];
}
}
&:not(:has(*:first-child)){
@apply -mt-1.5;
}
}
}
</style>
+92
View File
@@ -0,0 +1,92 @@
<template>
<div class="user-profile-card">
<img :src="profilePhoto" />
<h2>{{ firstName }} {{ lastName }}</h2>
<ul>
<li v-for="(value, key) in data" :key="key">
<label>{{ $t(key) }}</label>
<span>{{ value }}</span>
</li>
</ul>
<template v-if="device == 'desktop'">
<router-link to="/panel/edit">
<Button v-bind="btns.edit.props" />
</router-link>
<Button v-bind="btns.security.props" @click="handleClick" />
</template>
</div>
</template>
<script setup>
import init from '../../initialize/userProfileCard.js'
const { btns, items } = init()
const { device, dialog } = inject('service')
const profile = inject('data')
const { profilePhoto = '/images/empty-profile.svg', firstName, lastName } = profile
const data = reactive({});
const temp = {
true: 'تایید شده', false: 'تایید نشده', 'WHOLESALER': 'عمده فروش', 'BUYER': 'خریدار', 'RETAILER': 'خرده فروش'
}
items.forEach(key => {
const value = profile[key];
data[key] = temp[value] || value
});
const handleClick = () =>
dialog.show(resolveComponent('PanelDialogPasswordChange'))
</script>
<style lang="scss">
.user-profile-card {
@apply bg-[#FAFAFA] rounded-[0.6rem] border-[#E5E5E5];
@apply flex flex-col gap-3 p-6 relative overflow-hidden;
@apply w-[21.4rem] h-[20.1rem] lg:w-[20.8rem] lg:h-[34.9rem] lg:border-2;
&::before {
@apply content-['_'] top-0 inset-x-0 h-[4.4rem] lg:h-[6.3rem] bg-primary-600 absolute;
}
img {
@apply w-20 h-20 lg:w-[7.5rem] lg:h-[7.5rem] rounded-full border-2 border-[#FAFAFA] z-0 self-center mt-4;
}
h2 {
@apply text-[#333333] text-sm lg:text-xl font-bold font-vazir self-center;
}
ul {
@apply flex flex-col gap-4 my-auto;
li {
@apply flex justify-between items-center pb-4 border-b border-dotted;
@apply text-xs lg:text-base font-medium font-vazir border-[#E5E5E5] last:border-0;
label {
@apply text-[#999999];
}
span {
@apply text-[#333333];
}
}
}
a {
@apply w-full flex;
}
button {
@apply w-full h-[2.8rem] lg:h-12 justify-center #{!important};
span:not(.isax) {
@apply grow-0 text-[0.7em] lg:text-sm font-medium font-iran-sans;
}
.isax {
@apply text-xl;
}
}
}
</style>
+57
View File
@@ -0,0 +1,57 @@
<template>
<div class="recent-factor-item">
<div>
<div>
<span>{{ $t("factor") }} :</span>
<span>{{ invoiceNumber }}</span>
</div>
<div>
<span>{{ $t("date") }} :</span>
<span>{{ new Date(createdAt).toLocaleDateString('fa-IR') }}</span>
</div>
</div>
<router-link :to="{ query: { id } }">
<Button icon="isax isax-arrow-left" severity="secondary" text rounded />
</router-link>
</div>
</template>
<script setup>
const props = defineProps(['data'])
const { _id: id, invoiceNumber, createdAt } = props.data
</script>
<style lang="scss">
.recent-factor-item {
@apply w-[19.9rem] sm:w-[16.8rem] h-20 lg:h-[5.6rem] flex gap-2 bg-[#F2F2F2] rounded-[0.6rem] p-4;
&>div {
@apply flex flex-col h-full justify-between gap-3;
&>div {
@apply flex gap-2 items-center font-vazir;
span:nth-of-type(1) {
@apply text-[#999999] text-xs lg:text-[1.1em];
}
span:nth-of-type(2) {
@apply text-[#333] text-xs lg:text-[1.1em] font-medium;
}
}
}
a {
@apply self-end mr-auto #{!important};
button {
@apply -m-2 w-10 h-10;
.p-button-icon {
@apply text-xl text-[#292D32] lg:text-2xl;
}
}
}
}
</style>
+198
View File
@@ -0,0 +1,198 @@
<template>
<div class="sales-factor">
<div>
<h2>{{ $t("salesFactor") }}</h2>
<ul>
<li v-for="(value, key) in data" :key="key">
<label>{{ $t(key) }}:</label>
<span>{{ value }}</span>
</li>
</ul>
</div>
<table>
<thead>
<tr>
<th v-for="(h, i) in headers" :key="i" v-text="$t(h)" />
</tr>
</thead>
<tbody>
<tr v-for="(item, i) in products" :key="i">
<td>{{ i + 1 }}</td>
<td>{{ item.product.uid }}</td>
<td>{{ item.product.title }}</td>
<td>{{ item.quantity }}</td>
<td>{{ $t('number') }}</td>
<td>{{ numberFormat(item.price) }}</td>
<td>{{ 100 - Math.round(((item.specialPrice || item.price) / item.price) * 100) }} %</td>
<td>{{ numberFormat(item.specialPrice || item.price) }}</td>
<td></td>
</tr>
</tbody>
</table>
<div>
<div>
<div>
<label v-for="(word, i) in words" :key="i">
{{ $t(word) }}:
</label>
</div>
<label>{{ $t("debtor") }}:</label>
</div>
<ul>
<li v-for="(value, key) in summary" :key="key">
<label>{{ $t(key) }}</label>
<span>{{ numberFormat(value) }}</span>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import init from '../../initialize/panel.js'
const { headers, words } = init()
const props = defineProps(['data'])
const { products, userID, _id, invoiceNumber, shippingAddress, createdAt } = props.data
const data = reactive({
customerName: userID.firstName + ' ' + userID.lastName,
phone: userID.phoneNumber,
documentNumber: _id,
customerCode: userID._id,
fax: userID.cellNumber,
factorNumber: invoiceNumber,
address: shippingAddress,
factorDate: new Date(createdAt).toLocaleDateString('fa-IR'),
});
const sumPrices = products.reduce((sum, item) => sum + item.price, 0)
const sumSpecialPrices = products.reduce((sum, item) => sum + item.specialPrice || item.price, 0)
const summary = reactive({
netSum: sumPrices,
discountPercent: 100 - Math.round((sumSpecialPrices / sumPrices) * 100),
sheetDiscount: sumPrices - sumSpecialPrices,
totalAmountFactor: sumSpecialPrices
});
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.sales-factor {
@apply w-[77.5rem] flex flex-col [&_*]:border-[#E5E5E5];
&>div:nth-of-type(1) {
@apply flex flex-col;
&>h2 {
@apply h-[2.8rem] bg-zinc-100 flex items-center justify-center;
@apply text-[#333333] text-sm lg:text-base font-bold font-vazir;
}
ul {
@apply grid grid-cols-3 grid-rows-3 py-8 px-6 gap-6 border text-xs lg:text-base;
li {
@apply flex items-center gap-2.5 font-vazir;
label {
@apply text-[#333333] font-bold;
}
span {
@apply text-[#7F7F7F] font-medium;
}
&:nth-child(7) {
@apply col-span-2;
}
}
}
}
table {
thead {
tr {
@apply h-[2.8rem] bg-[#F2F2F2];
th {
@apply border border-t-0;
@apply text-center text-[#333333] text-xs lg:text-base font-medium font-vazir;
}
th:nth-of-type(9) {
@apply text-right pr-3;
}
}
}
tbody {
tr {
@apply h-[2.8rem];
td {
@apply border border-t-0;
@apply text-center text-[#333333] text-[0.7em] lg:text-sm font-medium font-vazir;
}
td:nth-of-type(3) {
@apply w-[22.5rem];
}
td:nth-of-type(9) {
@apply w-[13.4rem];
}
}
}
}
&>div:nth-of-type(2) {
@apply w-[64.1rem] flex;
label {
@apply text-[#333333] text-xs lg:text-base font-bold font-vazir;
}
&>div {
@apply w-[43.1rem] flex flex-col border border-t-0;
&>div {
@apply h-[6.9rem] p-6 flex flex-wrap gap-y-4 border-b;
label {
@apply first:w-full last:mx-auto;
}
}
&>label {
@apply h-[10.2rem] flex items-end p-6;
}
}
&>ul {
@apply flex flex-col justify-end h-[17.1rem] border-l;
li {
@apply flex border-b first:border-t;
* {
@apply w-[10.5rem] h-[3.3rem] flex items-center justify-center;
}
label {
@apply border-l;
}
span {
@apply text-[#333333] text-[0.7em] lg:text-sm font-medium font-vazir;
}
}
}
}
}
</style>
+83
View File
@@ -0,0 +1,83 @@
<template>
<nav class="panel-toolbar">
<ul>
<li v-for="(item, i) in menu" :key="i">
<router-link :to="item.url" v-if="i < 7 || user.profile.confirmToSales">
<Button v-bind="getBind(item)" @click="toFloat(i)" />
</router-link>
</li>
</ul>
<span ref="selector" />
<Button v-bind="logoutBtn.props" @click="logout()" />
</nav>
</template>
<script setup>
import init from '../../initialize/panelToolbar.js'
const { menu, logoutBtn, getBind, floating } = init()
const route = useRoute()
const selector = ref()
const { dialog } = inject('service')
const logout = () => dialog.show(resolveComponent('PanelDialogLogout'))
const toFloat = (index = menu.findIndex((item) => item.url == route.path)) => {
try {
floating(menu, selector, index ??= 7)
} catch (error) {
}
}
const user = useUserStore()
user.get()
watch(() => route.path, () => toFloat())
onMounted(() => toFloat())
</script>
<style lang="scss">
.panel-toolbar {
@apply w-full container bg-[#FAFAFA] rounded-[0.6rem] border-2 border-[#E5E5E5];
@apply flex flex-wrap items-center justify-between mx-auto px-2 xl:px-4 2xl:px-6 xl:py-2 2xl:py-4 relative mt-10;
ul {
@apply flex xl:gap-4 2xl:gap-[1.1rem] overflow-hidden;
li {
button {
@apply h-10 py-1.5 justify-end overflow-visible whitespace-nowrap px-3 xl:px-3.5 2xl:px-4;
.p-button-icon {
@apply text-[#7F7F7F] lg:text-lg xl:text-xl 2xl:text-2xl 2xl:ml-2;
}
.p-button-label {
@apply text-[#7F7F7F] lg:text-sm xl:text-base 2xl:text-lg font-medium font-vazir grow-0;
}
&[active] * {
@apply text-primary-600;
}
}
}
}
&>button {
@apply h-10 xl:h-12 2xl:h-[3.7rem] bg-[#F2F2F2] hover:bg-[#eaeaea] active:bg-[#d8d8d8] px-3 xl:px-4 2xl:px-5 mr-auto #{!important};
.p-button-icon {
@apply text-[#AD3434] lg:text-base xl:text-lg 2xl:text-2xl xl:ml-4;
}
.p-button-label {
@apply text-[#AD3434] lg:text-sm xl:text-base 2xl:text-lg font-medium font-vazir grow-0;
}
}
&>span {
@apply content-['_'] right-6 h-1 rounded-full bg-primary-600 absolute -bottom-[0.2rem];
@apply transition-['right'] duration-200;
}
}
</style>
+71
View File
@@ -0,0 +1,71 @@
<template>
<div class="logout">
<h2>
<i class="isax isax-logout" />
<span>{{ $t("logoutUserAccount") }}</span>
</h2>
<p>{{ $t("logoutUserAccountQuest") }}</p>
<div>
<Button v-bind="btns.logout.props" @click="logout()" />
<Button v-bind="btns.cancel.props" @click="dialog.close()" />
</div>
</div>
</template>
<script setup>
import init from '../../../initialize/logout.js'
const { btns } = init();
const dialog = inject('dialogRef');
const router = useRouter()
const logout = () => {
const token = useCookie('token')
token.value = null
router.push('/')
dialog.value.close()
}
</script>
<style lang="scss">
.logout {
@apply bg-white w-full lg:w-[32.1rem] h-64 flex flex-col items-center gap-2 rounded-[0.6rem] max-lg:rounded-b-none p-[2.4rem];
h2 {
@apply flex items-center gap-4 text-[#333];
i {
@apply text-[2em];
}
span {
@apply text-xl font-bold font-vazir leading-[2.8rem];
}
}
p {
@apply text-[#B2B2B2] text-base font-medium font-vazir leading-[1.7rem];
}
div {
@apply flex items-center gap-4 mt-auto;
button {
@apply w-36 lg:w-[10.4rem] h-11 border-[#AD3434] #{!important};
.p-button-label {
@apply font-medium font-vazir leading-tight #{!important};
}
}
button:first-child {
@apply bg-[#AD3434];
}
button:last-child .p-button-label {
@apply text-[#AD3434];
}
}
}
</style>
+160
View File
@@ -0,0 +1,160 @@
<template>
<dialog class="panel-otp-code-enter">
<Button v-bind="btns.close.props" @click="dialogRef.close()" />
<h2>
<i class="isax isax-message-notif" />
<span>{{ $t("enterReceivedCode") }}</span>
</h2>
<p>{{ $t("enterReceivedCodeDesc") }}</p>
<ul ref="list">
<li v-for="(_, i) in 6" :key="i">
<InputNumber
v-model="code[i]"
@input="events.input($event, i)"
@keyup="events.keyup($event, i)"
@focus="events.focus($event, i)"
:disabled="inputs.length < 1"
/>
</li>
</ul>
<span :class="{ invisible: seconds < 1 }">
{{ $t("moreSecondReceiveCode", { seconds }) }}
</span>
<Button v-bind="btns.next.props" @click="events.next()" />
<div v-show="seconds < 1">
<p>{{ $t("notReceivedCode") }}</p>
<Button :label="$t('tryAgain')" link @click="events.try()" />
</div>
</dialog>
</template>
<script setup>
const { btns } = inject('init');
const seconds = ref(0);
const code = ref([]);
const form = reactive({});
const userStore = useUserStore();
const optCodeStore = useOtpCodeStore();
const list = ref();
let inputs = reactive([]);
const events = reactive({
next: async () => {
if (code.value.length == 6) {
if (optCodeStore.type == "email") form.email = optCodeStore.email;
else form.phoneNumber = optCodeStore.number;
form.code = code.value.join("");
const { status } = await userStore.forgetPassword(form);
// if (status == "success")
emit("dialog", "PanelPasswordRenewal");
}
},
try: async () => {
const form = { number: otpCode.number };
const { status } = await otpCode.first(form);
if (status == "success") seconds.value = 30;
},
input: (e, i) => {
if (e.value != null) inputs[i++].value = e.originalEvent.key;
if (i < 6) inputs[i].focus();
else {
inputs[5].blur();
events.next();
}
},
keyup: (e, i) => {
if (e.code == "Backspace" && i > 0) {
code.value[i - 1] = null;
inputs[i - 1].focus();
}
},
focus: (e, i) => {
const length = code.value.filter((c) => typeof c == "number").length;
if (length < i) inputs[length].focus();
},
});
onMounted(() => {
inputs.push(...list.value.querySelectorAll("input"));
setTimeout(() => inputs[0].focus(), 1);
seconds.value = 30;
setInterval(() => {
if (seconds.value > 0) seconds.value--;
}, 1000);
});
</script>
<style lang="scss">
.panel-otp-code-enter {
@apply w-full lg:w-[47.9rem] lg:h-[31.2rem] bg-white rounded-t-2xl lg:rounded-2xl flex flex-col items-center p-6 lg:p-8 relative;
&::after {
@apply content-['_'] absolute inset-x-0 bottom-[7.2rem] h-3 shadow-[0px_-2px_5px_#0000000a] lg:shadow-none;
}
& > button:nth-of-type(1) {
@apply self-end -mt-2 -ml-2;
.p-button-icon {
@apply text-sm text-[#333333] before:content-['\e90b'];
@apply lg:text-[2em] lg:text-[#b2b2b29b] lg:before:content-['\e90c'];
}
}
& > h2 {
@apply flex items-center gap-4 -mt-12 lg:mt-0.5 max-lg:self-start;
i {
@apply max-lg:hidden text-[2em];
}
span {
@apply text-[#333333] text-sm lg:text-xl font-bold font-vazir leading-[2.8rem];
}
}
& > p {
@apply self-start w-[18.1rem] text-[#B2B2B2] text-[0.7em] font-vazir mb-5;
@apply lg:w-[23.2rem] lg:text-base lg:mt-2 lg:mb-0 lg:leading-snug lg:self-center lg:text-center;
}
& > ul {
@apply flex gap-2.5 mt-4 lg:mt-10 ltr;
li {
input {
@apply w-9 h-9 lg:w-10 lg:h-10 p-0 rounded-lg border border-neutral-200 text-center;
@apply focus:text-primary-600 lg:text-xl font-medium font-vazir leading-[1.9rem];
}
}
}
& > span {
@apply text-[#999999] text-[0.7em] lg:text-sm font-medium font-vazir mt-5 lg:mt-4;
}
& > button:nth-of-type(2) {
@apply w-[21.4rem] lg:w-[19.5rem] h-[2.9rem] lg:h-14 mt-8 lg:mt-11;
.p-button-label {
@apply text-sm lg:text-base font-bold font-vazir leading-tight;
}
}
& > div {
@apply flex items-center -mb-3 whitespace-nowrap;
p {
@apply text-[#999999] text-sm font-medium font-vazir;
}
button {
.p-button-label {
@apply text-sm font-bold font-vazir;
}
}
}
}
</style>
+173
View File
@@ -0,0 +1,173 @@
<template>
<dialog class="panel-password-change">
<img src="/images/password-change.svg" />
<div>
<Button v-bind="btns.close.props" @click="dialogRef.close()" />
<h2>{{ $t("changePassword") }}</h2>
<p>{{ $t("enterCurrentAndNewPassword") }}</p>
<ul>
<li v-for="(item, key) in fields.changePassword" :key="key">
<Password v-model="form[key]" v-bind="item.props" :inputId="key" />
<label :for="key"> {{ $t(key) }} </label>
<small v-if="errors[key]">{{ errors[key] }}</small>
</li>
</ul>
<Button v-bind="btns.save.props" :loading="loading" @click="save()" />
<div>
<p>{{ $t("forgetPassword") }}</p>
<Button v-bind="btns.recovery.props" @click="recovery()" />
</div>
</div>
</dialog>
</template>
<script setup>
const { meta } = useRoute()
const { btns, fields } = meta.init;
const { device, dialog, toast, t } = inject('service')
const dialogRef = inject('dialogRef')
const form = reactive({});
const errors = ref({});
const loading = ref(false);
btns.save.props.disabled = computed(() => Object.keys(errors.value).length > 1)
const save = async () => {
loading.value = true
const { status, data, error } = await useFetch('/api/users/setpass', {
headers: { Authorization: useCookie('token') }, method: 'post', body: Object.assign({}, form)
})
if (status.value == 'success') {
const token = useCookie('token')
token.value = data.value.accessToken
toast.add({
life: 3000, severity: 'success', summary: t('success'), detail: t('updatePasswordSuccessfull')
})
dialogRef.value.close()
} else {
if (error.value.statusCode == 422)
errors.value = error.value.data
else
toast.add({
life: 3000, severity: 'error', summary: t('error'), detail: error.value.message
})
}
loading.value = false
}
const recovery = () => {
if (device.value == 'desktop')
dialogRef.value.close()
dialog.show('PanelDialogPasswordRestore')
}
watch(computed(() => form), (value, old) => {
Object.keys(errors.value).forEach(key => {
if (value[key] != old[key]) delete errors.value[key]
});
})
</script>
<style lang="scss">
.panel-password-change {
@apply w-full lg:w-[61.6rem] lg:h-[36.9rem] bg-white rounded-t-2xl lg:rounded-2xl flex p-6 lg:p-4 lg:pl-6 relative;
&::after {
@apply content-['_'] absolute inset-x-0 bottom-28 h-3 shadow-[0px_-2px_5px_#0000000a] lg:shadow-none;
}
&>img {
@apply hidden lg:block w-[27.2rem];
}
&>div {
@apply flex flex-col gap-4 lg:pt-2 items-center w-full;
&>h2 {
@apply self-start -mt-[3.4rem] text-[#333333] text-sm font-bold font-vazir leading-[1.7rem];
@apply lg:text-white lg:text-xl lg:-mt-10 lg:-mr-[25.3rem];
}
&>p {
@apply self-start w-[17.8rem] -mt-2 text-[#7F7F7F] text-[0.7em] font-vazir mb-5;
@apply lg:w-[22.9rem] lg:text-[#E5E5E5] lg:text-base lg:leading-[1.7rem] lg:-mr-[25.3rem];
}
&>button:nth-of-type(1) {
@apply self-end -mt-2 -ml-2;
.p-button-icon {
@apply text-sm text-[#333333];
@apply lg:text-[2em] lg:text-[#b2b2b29b];
}
}
&>ul {
@apply flex flex-col items-center gap-6 mt-auto w-full sm:w-auto lg:-mt-16;
li {
@apply w-[21.4rem] lg:w-[23.3rem] h-[3.4rem] lg:h-14 relative;
label {
@apply w-max absolute right-3 top-4 px-1 bg-white transition-['top'] pointer-events-none;
@apply text-neutral-400 text-sm lg:text-base font-medium cursor-text font-vazir;
}
.p-password {
input {
@apply w-full rounded-[0.6rem] border text-sm lg:text-base shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
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;
}
}
small {
@apply text-red-500 font-vazir float-left;
}
&:has(small) * {
@apply border-red-500 #{!important};
}
}
}
&>button:nth-of-type(2) {
@apply w-[21.4rem] lg:w-[23.3rem] h-[2.9rem] mt-4 lg:h-14 lg:mt-16 #{!important};
.p-button-label {
@apply font-bold font-vazir leading-tight text-sm lg:text-base;
}
}
&>div {
@apply flex items-center justify-center -my-4 hidden;
p {
@apply text-[#999999] text-xs lg:text-sm font-medium font-vazir;
}
button {
@apply px-2;
.p-button-label {
@apply text-xs lg:text-sm font-bold font-vazir text-primary-600;
}
}
}
}
}
</style>
@@ -0,0 +1,103 @@
<template>
<dialog class="panel-password-renewal">
<Button v-bind="btns.close.props" @click="$emit('close')" />
<h2>
<i class="isax isax-lock-1"></i>
<span>{{ $t("newPassword") }}</span>
</h2>
<p>{{ $t("passwordMustBeCombination") }}</p>
<ul>
<li v-for="(item, key) in fields" :key="key">
<Password v-model="form[key]" v-bind="item.props" :inputId="key" />
<label :for="key"> {{ $t(key) }} </label>
</li>
</ul>
<Button v-bind="btns.save.props" @click="save()" />
</dialog>
</template>
<script setup>
const { btns, fields } = inject('init');
const form = reactive({});
const userStore = useUserStore();
const save = async () => {
const result = await userStore.resetPassword(form);
}
</script>
<style lang="scss">
.panel-password-renewal {
@apply w-full lg:w-[47.9rem] lg:h-[31.2rem] bg-white rounded-t-2xl lg:rounded-2xl flex flex-col items-center p-6 lg:p-8;
&::after {
@apply content-['_'] absolute inset-x-0 bottom-[4.5rem] h-3 shadow-[0px_-2px_5px_#0000000a] lg:shadow-none;
}
&>button:nth-of-type(1) {
@apply self-end -mt-2 -ml-2;
.p-button-icon {
@apply text-sm text-[#333333] before:content-['\e90b'];
@apply lg:text-[2em] lg:text-[#b2b2b29b] lg:before:content-['\e90c'];
}
}
&>h2 {
@apply flex items-center gap-4 -mt-12 lg:mt-0.5 max-lg:self-start;
i {
@apply max-lg:hidden text-[2em];
}
span {
@apply text-[#333333] text-sm lg:text-xl font-bold font-vazir leading-[2.8rem];
}
}
&>p {
@apply self-start w-[18.1rem] text-[#B2B2B2] text-[0.7em] font-vazir mb-5;
@apply lg:w-[23.2rem] lg:text-base lg:mt-2 lg:mb-0 lg:leading-snug lg:self-center lg:text-center;
}
&>ul {
@apply flex flex-col gap-6 mt-4 lg:mt-10 w-full items-center;
li {
@apply w-[21.4rem] lg:w-[23.3rem] lg:h-14 relative;
label {
@apply w-max absolute right-3 top-4 px-1 bg-white transition-['top'] pointer-events-none;
@apply text-neutral-400 text-sm lg:text-base font-medium cursor-text font-vazir;
}
.p-password {
input {
@apply rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
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;
}
}
}
}
&>button:nth-of-type(2) {
@apply w-[21.4rem] lg:w-[23.3rem] h-[2.9rem] lg:h-14 mt-8 lg:mt-10;
.p-button-label {
@apply text-sm lg:text-base font-bold font-vazir leading-tight;
}
}
}
</style>
@@ -0,0 +1,119 @@
<template>
<dialog class="pamel-password-restore">
<Button v-bind="closeBtn.props" @click="$emit('close')" />
<h2>
<i class="isax isax-shield-search"></i>
<span>{{ $t("passwordRecovery") }}</span>
</h2>
<p>
{{ $t("recoverPasswordByEmailOrMobile") }} <br />
{{ $t("pleaseSelectMethod") }}
</p>
<SelectButton v-model="way" :options="ways">
<template #option="{ option }">
<span>{{ $t(option.replace("number", "sms")) }}</span>
</template>
</SelectButton>
<div>
<InputText v-model="form[way]" id="input" placeholder />
<label for="input">{{ $t(labels[way]) }}</label>
</div>
<Button v-bind="recoveryBtn.props" @click="events.recovery()" />
</dialog>
</template>
<script setup>
import init from "../../../../initialize/panelPasswordRestore";
const { closeBtn, ways, labels, fields, recoveryBtn } = init();
const emit = defineEmits(["dialog"]);
const way = ref("email");
const form = reactive({});
const validator = useValidator();
const optCode = useOtpCodeStore();
const events = reactive({
recovery: async () => {
if (validator.isValid(fields, form)) {
const { status } = await optCode.send(way.value, form);
if (status == "success") emit("dialog", "PanelOTPCodeEnter");
}
},
});
</script>
<style lang="scss">
.pamel-password-restore {
@apply w-full lg:w-[47.9rem] lg:h-[31.2rem] bg-white rounded-t-2xl lg:rounded-2xl flex flex-col items-center p-6 lg:p-8 relative;
&::after {
@apply content-['_'] absolute inset-x-0 bottom-[4.5rem] h-3 shadow-[0px_-2px_5px_#0000000a] lg:shadow-none;
}
& > button:nth-of-type(1) {
@apply self-end -mt-2 -ml-2;
.p-button-icon {
@apply text-sm text-[#333333] before:content-['\e90b'];
@apply lg:text-[2em] lg:text-[#b2b2b29b] lg:before:content-['\e90c'];
}
}
& > h2 {
@apply flex items-center gap-4 -mt-12 lg:mt-0.5 max-lg:self-start;
i {
@apply max-lg:hidden text-[2em];
}
span {
@apply text-[#333333] text-sm lg:text-xl font-bold font-vazir leading-[2.8rem];
}
}
& > p {
@apply self-start w-[18.1rem] text-[#B2B2B2] text-[0.7em] font-vazir mb-5;
@apply lg:w-[23.2rem] lg:text-base lg:mt-2 lg:mb-0 lg:leading-snug lg:self-center lg:text-center;
}
& > div:nth-of-type(1) {
@apply w-[21.4rem] lg:w-[23.3rem] h-[3.4rem] rounded-lg border border-[#E5E5E5] mt-1.5 flex items-center p-1 gap-1;
& > div {
@apply flex justify-center items-center p-0 text-sm lg:text-base;
@apply w-[11.4rem] h-[2.9rem] rounded-[0.4rem] border-none text-neutral-400 hover:text-neutral-400;
&.p-highlight {
@apply bg-neutral-200 text-zinc-800 hover:bg-neutral-200 hover:text-zinc-800;
}
}
}
& > div:nth-of-type(2) {
@apply w-[21.4rem] lg:w-[23.3rem] h-[3.4rem] lg:h-14 relative mt-[1.9rem] lg:mt-10;
label {
@apply w-max absolute right-3 top-4 px-1 bg-white transition-['top'] pointer-events-none;
@apply text-neutral-400 text-sm lg:text-base font-medium font-vazir cursor-text;
}
input {
@apply w-full rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir;
}
&:has(input:focus),
&:has(input:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal font-vazir;
}
}
}
& > button:nth-of-type(2) {
@apply w-[21.4rem] lg:w-[23.3rem] h-12 lg:h-14 mt-8 lg:mt-6;
.p-button-label {
@apply font-bold font-vazir leading-tight text-sm lg:text-base;
}
}
}
</style>
+156
View File
@@ -0,0 +1,156 @@
<template>
<DataTable :value="muchSell" class="seller-best-selling" :responsiveLayout="rl">
<template #header>
<h2>{{ $t('bestsellingPorducts') }}</h2>
<Dropdown v-model="period" :options="options" optionValue="value" optionLabel="label">
<template #dropdownicon>
<i class="isax isax-arrow-bottom" />
</template>
</Dropdown>
</template>
<Column v-for="(col, i) in columns[device]" :key="i" v-bind="col"
class="!text-center [&:first-child_button]:hidden">
<template #body="{ data, field }">
<img v-if="device == 'desktop' && i === 1" :src="field(data)" alt="">
<template v-else>
<span>{{ field(data) }}</span>
<Button v-if="device == 'mobile'" :label="$t('moreDetails')" icon="isax isax-arrow-left-2" link @click="show(data)"/>
</template>
</template>
</Column>
<template #empty>
<p>{{ $t('notFound') }}</p>
</template>
</DataTable>
</template>
<script setup>
import PanelSellerDialogDetailsBestSelling from './dialog/details/BestSelling.vue'
const { device, dialog, t } = inject('service')
const rl = computed(() => device.value == 'mobile' ? 'stack' : '')
const { muchSell = [] } = inject('data')
const columns = reactive({
desktop: [
{ header: t('row'), field: (data) => muchSell.indexOf(data) + 1 },
{ header: t('image'), field: (data) => data.product.coverImage },
{ header: t('productTitle'), field: (data) => data.product.title },
{ header: t('salesPrice'), field: (data) => numberFormat(data.total) + ' ' + t('priceUnit') },
{ header: t('inventory'), field: (data) => data.quantity },
],
mobile: [
{ field: (data) => data.product.title },
{ field: (data) => numberFormat(data.total) + ' ' + t('priceUnit') },
]
})
const options = reactive([
{ label: t('thisWeek'), value: 'weekly' }
])
const period = ref('weekly')
const show = (data) => {
dialog.show(PanelSellerDialogDetailsBestSelling, data)
}
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.seller-best-selling {
@apply rtl border-2 border-[#E5E5E5] rounded-[0.6rem] overflow-hidden w-full;
.p-datatable-header {
@apply flex items-center justify-between bg-white pt-3 pl-3 pr-[1.1rem] pb-[1.1rem] lg:py-2.5 lg:px-5;
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir;
}
.p-dropdown {
@apply w-[5.6rem] h-[2.4rem] lg:w-[6.5rem] lg:h-[2.6rem] bg-[#F2F2F2] rounded-[0.6rem] text-[#5D5D5D] border-none;
.p-dropdown-trigger {
@apply w-max translate-x-2;
i {
@apply text-base lg:text-lg;
}
}
span {
@apply p-3 h-max w-max pl-0 font-vazir text-xs lg:text-sm #{!important};
}
}
}
.p-datatable-wrapper {
@apply max-lg:overflow-hidden #{!important};
.p-datatable-table {
@apply border-separate border-spacing-y-[0.8rem] px-[1.1rem] py-3;
.p-datatable-thead {
@apply w-full h-[3.1rem] lg:px-5 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 max-lg:w-full max-lg:px-3 text-right lg:text-center;
@apply lg:first:rounded-r-[0.6rem] lg:last:rounded-l-[0.6rem] py-0 lg:h-[4.5rem];
&>span {
@apply max-lg:py-3 min-h-[3.8rem] flex items-center text-xs lg:text-base text-[#333333] font-vazir;
}
img {
@apply w-[3.75rem] h-[3.75rem];
}
@media(max-width: 1024px) {
&>button {
@apply -m-3 mr-auto #{!important};
.p-button-label {
@apply text-xs font-medium font-vazir whitespace-nowrap #{!important};
}
.p-button-icon {
@apply text-sm mr-1;
}
}
&: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
}
}
}
}
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<div class="seller-management">
<h2>{{ $t(title + 'Mangement') }}</h2>
<ul>
<li v-for="(item, i) in items[title]" :key="i">
<small>{{ item.label }}</small>
<span>{{ item.value }}</span>
</li>
</ul>
</div>
</template>
<script setup>
const props = defineProps(['title'])
const {
itemHighStock, zeroStock, returnedProducts, itemLowStock,
oneWeekAgo, oneDayAgo, pendingProducts = [], receivedProducts,
} = inject('data')
const items = reactive({
warehouse: [
{ label: 'کالاهای موجود', value: itemHighStock },
{ label: 'کالاهای بدون موجودی', value: zeroStock },
{ label: 'کالاهای مرجوعی یک هفته گذشته', value: returnedProducts },
{ label: 'کالاهای در حال اتمام موجودی', value: itemLowStock },
],
orders: [
{ label: 'سفارشات این هفته', value: oneWeekAgo },
{ label: 'سفارش های امروز', value: oneDayAgo },
{ label: 'سفارش های پذیرش نشده', value: pendingProducts.length },
{ label: 'سفارش های ارسال شده', value: receivedProducts },
]
})
</script>
<style lang="scss">
.seller-management {
@apply w-full md:w-[21.4rem] lg:w-[28.1rem] flex flex-col pt-[1.1rem] gap-[1.1rem] lg:pt-5 lg:gap-5 rounded-[0.6rem] border-2 border-neutral-200;
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir pr-[1.1rem] lg:pr-5;
}
ul {
@apply w-full flex flex-col gap-6 lg:gap-[1.9rem] p-[1.1rem] lg:pt-6 border-t border-[#E5E5E5];
li {
@apply flex justify-between items-center text-[#333333] font-medium font-vazir;
small {
@apply text-xs lg:text-base;
}
span {
@apply text-sm lg:text-xl;
}
}
}
}
</style>
+46
View File
@@ -0,0 +1,46 @@
<template>
<ul class="seller-menu">
<li v-for="(item, i) in items" :key="i">
<router-link :to="item.link" v-ripple>
<i :class="item.icon" />
<span>{{ item.label }}</span>
</router-link>
</li>
</ul>
</template>
<script setup>
const items = reactive([
{ link: '/panel/seller/store', label: 'فروشگاه', icon: 'isax isax-shop' },
{ link: '/panel/seller/products', label: 'همه کالاها', icon: 'isax isax-bag' },
{ link: '/panel/seller/new-product', label: 'معرفی کالا', icon: 'isax isax-bag-cross' },
{ link: '/panel/seller/orders', label: 'سفارشات', icon: 'isax isax-bag-tick-2' },
])
</script>
<style lang="scss">
.seller-menu {
@apply flex flex-col gap-[0.3rem] pt-4 border-t border-[#E5E5E5];
a {
@apply w-72 h-[3.2rem] py-1.5 pr-[1.1rem] flex items-center gap-[0.9rem] text-[#333333] overflow-hidden relative;
&.router-link-active {
@apply bg-[#47B556]/20 text-[#47B556];
&:after {
@apply content-[''] absolute right-0 h-[2.4rem] w-[0.2rem] rounded-tl-sm rounded-bl-sm bg-[#47B556];
}
}
i {
@apply text-2xl;
}
span {
@apply text-base font-medium font-vazir
}
}
}
</style>
+134
View File
@@ -0,0 +1,134 @@
<template>
<div class="seller-order">
<div>
<div>
<img :src="coverImage" alt="">
<h2>{{ title }}</h2>
</div>
<div>
<span>{{ $t('number') }} : {{ data.quantity }}</span>
<span>{{ $t('amount') }} : {{ numberFormat(data.price) }}</span>
</div>
</div>
<div>
<Tag v-bind="tag" rounded />
<div v-if="status == 'Pending'">
<Button icon="isax isax-tick-square" link severity="secondary" :loading="accepting" @click="accept()" />
<Button icon="isax isax-close-square" link severity="secondary" :loading="canceling"
@click="cancel()" />
</div>
<small v-else>{{ new Date(sendDate).toLocaleDateString('fa-IR') }}</small>
</div>
</div>
</template>
<script setup>
const props = defineProps(['data'])
let { product, status, _id: id, sendDate } = props.data
const { coverImage, title } = product
const { t } = inject('service')
const tags = reactive({
pending: { severity: 'warn', value: t('pending') },
accepted: { severity: 'info', value: t('accepted') },
received: { severity: 'success', value: t('received') },
returned: { severity: 'error', value: t('returned') },
canceled: { severity: 'error', value: t('canceled') },
})
const tag = ref(tags[status.toLowerCase()])
const accepting = ref(false)
const canceling = ref(false)
const accept = async () => {
accepting.value = true
const result = await useFetch('/api/shop/product/accept/' + id, {
headers: {
Authorization: useCookie('token')
}
})
accepting.value = false
if (result.status.value == 'success') {
status = 'accepted'
tag.value = tags[status]
sendDate = Date.now()
}
}
const cancel = async () => {
canceling.value = true
const result = await useFetch('/api/shop/product/cancel/' + id, {
headers: {
Authorization: useCookie('token')
}
})
canceling.value = false
if (result.status.value == 'success') {
status = 'canceled'
tag.value = tags[status]
sendDate = Date.now()
}
}
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.seller-order {
@apply w-full h-[12.1rem] rounded-[0.6rem] border border-[#E5E5E5] px-[1.1rem] lg:px-[1.9rem] flex flex-col;
&>div:first-child {
@apply w-full flex max-lg:flex-col max-lg:gap-[1.9rem] justify-between items-center py-[1.1rem] lg:py-5 border-b border-[#E5E5E5];
&>div:first-child {
@apply flex items-center gap-1.5 lg:gap-5;
img {
@apply w-[3.8rem] h-[3.8rem] lg:w-20 lg:h-20 bg-gray-200 shrink-0;
}
h2 {
@apply text-[#333333] text-sm lg:text-base font-medium font-vazir;
}
}
&>div:last-child {
@apply max-lg:w-full lg:h-full flex lg:flex-col justify-between items-end;
span {
@apply text-[#333333] text-xs lg:text-base font-normal font-vazir;
}
}
}
&>div:last-child {
@apply w-full h-full flex justify-between items-center;
.p-tag {
@apply h-[1.8rem];
}
&>div {
@apply flex gap-1.5 lg:gap-3;
.p-button {
@apply p-0 w-max h-max #{!important};
.p-button-icon {
@apply text-xl lg:text-2xl text-[#333333];
}
}
}
&>small {
@apply text-[#333333] text-sm lg:text-base font-vazir;
}
}
}
</style>
+101
View File
@@ -0,0 +1,101 @@
<template>
<div class="seller-chart">
<Dropdown v-model="period" :options="options" optionValue="value" optionLabel="label">
<template #dropdownicon>
<i class="isax isax-arrow-bottom" />
</template>
</Dropdown>
<Chart type="pie" :data="chartData" :options="chartOptions" />
</div>
</template>
<script setup>
import Chart from 'primevue/chart';
const chartData = ref();
const chartOptions = ref();
const setChartData = () => {
const documentStyle = getComputedStyle(document.body);
return {
labels: ['سود', 'درآمد', 'فروش'],
datasets: [
{
data: [21451000, 20123000, 10000000],
backgroundColor: ['#FF6384', '#FF9F40', '#4BC0C0'],
hoverBackgroundColor: ['#FF6384cc', '#FF9F40cc', '#4BC0C0cc']
}
]
};
};
const setChartOptions = () => {
return {
plugins: {
legend: {
labels: {
usePointStyle: true,
color: '#333333',
pointStyle: 'rect',
pointStyleWidth: 20,
font: {
family: 'vazir',
size: 14
}
},
position: 'bottom',
rtl: true
},
tooltip: {
backgroundColor: '#050B20',
bodyFont: 'vazir',
bodyColor: 'white',
rtl: true,
displayColors: false,
callbacks: {
title: () => null
}
}
}
};
};
const { t } = inject('service')
const options = reactive([
{ label: t('thisWeek'), value: 'weekly' }
])
const period = ref('weekly')
onMounted(() => {
chartData.value = setChartData();
chartOptions.value = setChartOptions();
});
</script>
<style lang="scss">
.seller-chart {
@apply w-[21.4rem] h-[25.4rem] lg:w-[18.3rem] lg:h-[22.8rem] rounded-[0.6rem] border-2 border-[#E5E5E5] p-3;
@apply flex flex-col gap-[1.1rem] max-xl:hidden lg:hidden xl:flex;
.p-dropdown {
@apply w-[5.6rem] h-[2.4rem] lg:w-[6.5rem] lg:h-[2.6rem] bg-[#F2F2F2] rounded-[0.6rem] text-[#5D5D5D] border-none;
.p-dropdown-trigger {
@apply w-max translate-x-2;
i {
@apply text-base lg:text-lg;
}
}
span {
@apply p-3 h-max w-max pl-0 font-vazir text-xs lg:text-sm #{!important};
}
}
}
</style>
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="seller-profile">
<span v-if="shop.rate > -1">
<small>{{ shop.rate }}</small>
<i class="isax isax-star-15" />
</span>
<div>
<picture>
<img :src="shop?.userID?.profilePhoto">
</picture>
<div>
<h2>{{ shop.shopName }}</h2>
<span>
{{ $t('registerDate') }} : {{ new Date(shop.createdAt).toLocaleDateString('fa-IR') }}
</span>
</div>
</div>
<PanelSellerMenu v-if="device == 'desktop'" />
</div>
</template>
<script setup>
const { device } = inject('service')
const { shop } = inject('data')
</script>
<style lang="scss">
.seller-profile {
@apply w-full md:w-[21.4rem] min-h-[6.4rem] lg:w-[18.3rem] lg:h-[30rem] rounded-[0.6rem] border-2 border-neutral-200 relative;
&>span {
@apply absolute left-4 top-4 lg:left-3 lg:top-3 flex items-center gap-2;
i {
@apply text-lg text-[#FFB628] -mt-0.5;
}
small {
@apply text-zinc-800 text-sm font-medium font-poppins;
}
}
&>div {
@apply flex lg:flex-col gap-[1.1rem] lg:gap-3 items-center max-lg:h-full max-lg:pr-4 lg:pt-7 lg:pb-4;
picture {
@apply flex w-[4.1rem] h-[4.1rem] lg:w-[5.6rem] lg:h-[5.6rem] rounded-full border border-[#47B556];
img {
@apply w-[3.6rem] h-[3.6rem] lg:w-[4.9rem] lg:h-[4.9rem] bg-zinc-300 rounded-full m-auto;
}
}
&>div {
@apply flex flex-col lg:items-center gap-5;
h2 {
@apply text-zinc-800 text-xs lg:text-base font-medium font-vazir;
}
span {
@apply text-zinc-500 text-[0.6em] lg:text-xs font-medium font-vazir;
}
}
}
}
</style>
+68
View File
@@ -0,0 +1,68 @@
<template>
<div class="seller-sales">
<h2>{{ $t(title + 'Sales') }}</h2>
<ul>
<li v-for="(item, i) in items[title]" :key="i">
<p>
{{ numberFormat(item.value) }} <span>{{ suffix[title] }}</span>
</p>
<span>{{ item.label }}</span>
</li>
</ul>
</div>
</template>
<script setup>
defineProps(['title'])
const {
salesCurrentWeek = 0, salesAgoWeek = 0, salseAgoMonth = 0,
oneDayAgo = 0, oneWeekAgo = 0, oneMonthAgo = 0
} = inject('data')
const items = reactive({
status: [
{ label: 'فروش هفته جاری', value: salesCurrentWeek },
{ label: 'فروش هفته گذشته', value: salesAgoWeek },
{ label: 'فروش ماه گذشته', value: salseAgoMonth },
],
number: [
{ label: 'تعداد کالای فروش رفته هفته جاری', value: oneDayAgo },
{ label: 'تعداد کالای فروش رفته هفته گذشته', value: oneWeekAgo },
{ label: 'تعداد کالای فروش رفته ماه گذشته', value: oneMonthAgo },
]
})
const suffix = reactive({ status: 'ریال', number: 'عدد' })
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss" scoped>
.seller-sales {
@apply w-full md:w-[21.4rem] lg:w-[28.1rem] flex flex-col pt-[1.1rem] gap-[1.1rem] lg:pt-5 lg:gap-5 rounded-[0.6rem] border-2 border-neutral-200;
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir pr-[1.1rem] lg:pr-5;
}
ul {
@apply w-full flex flex-col gap-6 lg:gap-[1.9rem] p-[1.1rem] lg:pt-6 border-t border-[#E5E5E5];
li {
@apply flex flex-col text-[#333333] [&>p]:first:text-[#47B556] font-medium font-vazir;
p {
@apply text-lg lg:text-2xl;
}
span {
@apply text-xs lg:text-base;
}
}
}
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template>
<div class="seller-score">
<h2>{{ $t('yourePerformanceScore') }}</h2>
<ul>
<li v-for="(item, i) in items" :key="i">
<small>{{ item.title }}</small>
<span>{{ item.percent }}%</span>
<svg viewBox="0 0 250 250" :style="{'--percent': item.percent }">
<circle v-for="j in 2" :key="j" />
</svg>
</li>
</ul>
</div>
</template>
<script setup>
const { shop } = inject('data')
const items = reactive([
{ title: 'رضایت مشتریان', percent: shop.rate * 10 },
{ title: 'تاخیر در ارسال', percent: shop.warn * 10 },
{ title: 'بازگشت کالا', percent: shop.returnedScore },
{ title: 'لغو سفارش', percent: shop.canceledScore },
])
</script>
<style lang="scss">
.seller-score {
@apply w-full md:w-[21.4rem] h-[10.1rem] lg:w-[18.3rem] lg:h-[21.9rem] flex flex-col pt-[1.1rem] gap-[1.1rem] lg:pt-5 lg:gap-5 rounded-[0.6rem] border-2 border-neutral-200;
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir pr-[1.1rem] lg:pr-5;
}
ul {
@apply w-full flex flex-wrap gap-2.5 lg:gap-x-[1.9rem] lg:gap-y-5 pt-[1.1rem] lg:pt-5 justify-center border-t border-[#E5E5E5];
li {
@apply w-[4.3rem] h-[4.3rem] lg:w-[6.9rem] lg:h-[6.9rem] relative flex flex-col items-center justify-center gap-2 rounded-full;
small {
@apply text-[#333333] text-[0.4em] lg:text-[0.6em] font-bold font-vazir mt-1;
}
span {
@apply text-[#333333] text-xs lg:text-base font-medium font-vazir;
}
svg {
@apply absolute inset-0;
--stroke-width: 5px;
@screen lg {
--stroke-width: 12px;
}
--radius: calc((250px - var(--stroke-width)) / 2);
circle {
cx: 125px;
cy: 125px;
r: var(--radius);
stroke-width: var(--stroke-width);
fill: none;
stroke-linecap: round;
}
circle:first-child {
stroke: #D9D9D9;
}
circle:last-child {
transition: stroke-dasharray 0.3s linear 0s;
stroke: #47B556;
transform-origin: 125px 125px;
--circumference: calc(var(--radius) * 3.14 * 2);
--dash: calc((var(--percent) * var(--circumference)) / 100);
stroke-dasharray: var(--dash) calc(var(--circumference) - var(--dash));
}
}
}
}
}
</style>
+212
View File
@@ -0,0 +1,212 @@
<template>
<DataTable :expandedRows="[]" :value="pendingProducts" class="seller-upcoming" :responsiveLayout="rl">
<template #header>
<h2>{{ $t('upcomingShipments') }}</h2>
<Dropdown v-model="period" :options="options" optionValue="value" optionLabel="label">
<template #dropdownicon>
<i class="isax isax-arrow-bottom" />
</template>
</Dropdown>
</template>
<Column v-for="(col, i) in columns[device]" :key="i" v-bind="col" class="[&:first-child_button]:hidden">
<template #body="{ data, field }">
<span>{{ field(data) }}</span>
<Button v-if="device == 'mobile'" :label="$t('moreDetails')" icon="isax isax-arrow-left-2"
link @click="show(data)" />
</template>
</Column>
<Column v-if="device == 'desktop'" expander />
<template #expansion="{ data }">
<ul>
<li>
<span v-if="data.quantity > 1">
{{ data.quantity }}x
</span>
<img :src="data.product.coverImage">
</li>
</ul>
<Button :label="$t('showFactor')" icon="isax isax-receipt-item" iconPos="right" link />
</template>
<template #empty>
<p>{{ $t('notFound') }}</p>
</template>
</DataTable>
</template>
<script setup>
import PanelSellerDialogDetailsOrder from './dialog/details/Order.vue'
const { t, device, dialog } = inject('service')
const rl = computed(() => device.value == 'mobile' ? 'stack' : '')
const { pendingProducts = [] } = inject('data')
const columns = reactive({
desktop: [
{ header: t('row'), field: (item) => pendingProducts.indexOf(item) + 1 },
{ header: t('purchaseDate'), field: (item) => new Date(item.orderDate).toLocaleDateString() },
{ header: t('postageDate'), field: (item) => new Date(item.sendDate).toLocaleDateString() },
{ header: t('price'), field: (item) => new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(item.price) },
],
mobile: [
{ field: (item) => item.product.title },
{ field: (item) => new Date(item.sendDate).toLocaleDateString() },
]
})
const options = reactive([
{ label: t('thisWeek'), value: 'weekly' }
])
const period = ref('weekly')
const show = (data) => {
dialog.show(PanelSellerDialogDetailsOrder, data)
}
</script>
<style lang="scss">
.seller-upcoming {
@apply rtl border-2 border-[#E5E5E5] rounded-[0.6rem] overflow-hidden w-full;
.p-datatable-header {
@apply flex items-center justify-between bg-white pt-3 pl-3 pr-[1.1rem] pb-[1.1rem] lg:py-2.5 lg:px-5;
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir;
}
.p-dropdown {
@apply w-[5.6rem] h-[2.4rem] lg:w-[6.5rem] lg:h-[2.6rem] bg-[#F2F2F2] rounded-[0.6rem] text-[#5D5D5D] border-none;
.p-dropdown-trigger {
@apply w-max translate-x-2;
i {
@apply text-base lg:text-lg;
}
}
span {
@apply p-3 h-max w-max pl-0 font-vazir text-xs lg:text-sm #{!important};
}
}
}
.p-datatable-wrapper {
@apply max-lg:overflow-hidden #{!important};
.p-datatable-table {
@apply border-separate border-spacing-y-[0.8rem] px-[1.1rem] py-3;
.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 max-lg:h-[7.5rem] bg-transparent relative #{!important};
&:not(.p-datatable-row-expansion)::after {
@apply content-['_'] absolute inset-x-0 z-0 max-lg:inset-y-0 lg:h-[3.8rem] rounded-[0.6rem];
@apply shadow-[0px_0px_6px_#0000001f];
}
td {
@apply max-lg:w-full max-lg:px-3 text-right last:text-left #{!important};
@apply lg:first:rounded-r-[0.6rem] lg:last:rounded-l-[0.6rem] py-0 align-top lg:border-none;
&>span {
@apply max-lg:py-3 min-h-[3.8rem] flex items-center text-xs lg:text-base text-[#333333] font-vazir;
}
.p-row-toggler {
@apply mt-3.5 z-10;
}
@media(max-width: 1024px) {
&>button {
@apply z-[1] -m-3 mr-auto #{!important};
.p-button-label {
@apply text-xs font-medium font-vazir;
}
.p-button-icon {
@apply text-sm mr-1;
}
}
&:first-child span {
@apply max-lg:w-full max-lg:border-b;
}
.p-column-title {
@apply hidden #{!important};
}
}
}
&.p-datatable-row-expansion {
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
animation: fade 0.2s ease 0.1s forwards;
@apply opacity-0 -translate-y-3 shadow-none;
ul {
@apply flex items-center gap-[1.1rem] rtl h-[9.9rem] border-y border-[#E5E5E5];
li {
@apply flex flex-col items-center justify-end h-[7.3rem] gap-3;
span {
@apply text-[#333333] text-base font-normal font-poppins
}
img {
@apply w-20 h-20;
}
}
}
button {
@apply float-left;
}
}
&:has(+.p-datatable-row-expansion)::after {
@apply h-[17.5rem] transition-[height];
}
}
.p-datatable-emptymessage p {
@apply lg:h-[3.8rem] flex justify-center items-center
}
}
}
}
}
</style>
@@ -0,0 +1,39 @@
<template>
<div class="upload-national-card" @click="showImages">
<img v-if="source" :src="source" />
<i v-else class="isax isax-document-upload"></i>
<input ref="input" type="file" hidden @input="chooseImage">
</div>
</template>
<script setup>
const file = defineModel()
const input = ref()
const source = ref()
const filename = ref()
const showImages = () => input.value.click();
const chooseImage = (e) => {
const binaryData = [];
binaryData.push(e.target.files[0]);
source.value = URL.createObjectURL(new Blob(binaryData, { type: "image" }))
file.value = e.target.files[0];
}
</script>
<style lang="scss">
.upload-national-card {
@apply cursor-pointer min-h-14 h-max relative w-[21.8rem] lg:w-[27.1rem] border border-[#CCCCCC] rounded-[0.6rem] p-4 rtl;
i {
@apply text-xl float-left;
}
img {
@apply w-full h-[11.6rem] lg:h-[14.4rem] object-cover object-center rounded-[0.6rem];
}
}
</style>
+120
View File
@@ -0,0 +1,120 @@
<template>
<div class="seller-add-spec">
<div>
<div>
<h2>{{ $t("addSpec") }}</h2>
<Button icon="isax isax-close-circle" severity="secondary" rounded link @click="dialog.close()" />
</div>
<p>{{ dialog.data.title }}</p>
</div>
<ul>
<li>
<InputText v-model="form.key" placeholder id="key" />
<label for="key">{{ $t('title') }}</label>
</li>
<li>
<Textarea v-model="form.value" placeholder id="value" />
<label for="value">{{ $t("description") }}</label>
</li>
</ul>
<Button :label="$t('submitSpec')" @click="add()" />
</div>
</template>
<script setup>
const dialog = inject('dialogRef')
const form = reactive({})
const add = () => {
dialog.value.data.specs[form.key] = form.value
dialog.value.close();
}
</script>
<style lang="scss">
.seller-add-spec {
@apply w-full lg:w-[40.1rem] lg:h-[38.6rem] bg-white rounded-t-[0.6rem] lg:rounded-[0.6rem] flex flex-col relative;
&::after {
@apply content-['_'] absolute inset-x-0 bottom-[4.5rem] h-3 shadow-[0px_-2px_5px_#0000000a] lg:shadow-none;
}
&>div {
@apply flex flex-col gap-1.5 lg:h-[7.8rem] lg:gap-3 border-b lg:border-b-2 border-[#E5E5E5] p-6 lg:px-10 lg:pt-8 pb-6;
&>div {
@apply flex justify-between items-center;
h2 {
@apply text-[#333333] text-sm lg:text-xl font-bold font-vazir;
}
button {
@apply -mx-4 -my-3;
.p-button-icon {
@apply text-[1.4em] lg:text-3xl text-[#191919];
}
}
}
p {
@apply text-[#7F7F7F] text-[0.7em] lg:text-lg font-normal font-vazir;
}
}
&>ul {
@apply flex flex-col h-full items-center gap-[1.1rem] px-6 pt-9 lg:gap-5 lg:pt-6 lg:px-10;
li {
@apply flex items-center gap-4 cursor-pointer w-max select-none relative;
@apply w-[21.4rem] lg:w-[27.1rem];
label {
@apply w-max absolute right-3 top-4 lg:top-3.5 px-1 bg-white transition-['top'];
@apply text-neutral-400 text-sm lg:text-base font-medium font-vazir cursor-text;
@apply pointer-events-none;
}
input, textarea {
@apply h-[3.4rem] pl-10 rtl w-full rounded-[0.6rem] border shadow-none;
@apply text-[#4C4C4C] font-vazir grow;
}
&:has(input:focus),
&:has(textarea:focus),
&:has(input:not(:placeholder-shown)),
&:has(textarea:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-[#4C4C4C] text-xs font-normal font-vazir;
}
}
}
li:last-child {
@apply w-full lg:w-[27.1rem] h-full flex flex-col items-start gap-4 mt-3 lg:mt-4;
// label {
// @apply text-[#7F7F7F] text-xs lg:text-xl font-medium font-vazir max-lg:mr-2.5;
// }
textarea {
@apply w-full h-[9.6rem] lg:h-full p-4 rounded-[0.6rem] border border-[#E5E5E5];
// &::placeholder {
// @apply text-[#999999] text-[0.7em] lg:text-lg font-normal font-vazir;
// }
}
}
}
&>button {
@apply h-[2.9rem] m-6 mt-8 lg:h-14 lg:m-10 lg:mt-9 shrink-0 #{!important};
.p-button-label {
@apply font-bold font-iran-sans text-sm lg:text-base;
}
}
}
</style>
@@ -0,0 +1,31 @@
<template>
<div class="seller-product-added">
<div>
<i class="isax isax-tick-square"></i>
<h2>{{ $t('productAdded') }}</h2>
</div>
<Button :label="$t('confirm')" />
</div>
</template>
<script setup>
</script>
<style lang="scss">
.eller-product-added {
@apply flex flex-col justify-between pt-[1.1rem] max-lg:h-[11.8rem] lg:w-[28.3rem] lg:h-64 lg:p-10;
div {
@apply flex max-lg:flex-col items-center gap-4 lg:mt-4;
h2 {
@apply text-[#333333] text-xl font-bold font-vazir leading-[2.8rem];
}
}
button {
@apply w-full h-12 lg:h-14;
}
}
</style>
@@ -0,0 +1,73 @@
<template>
<div class="seller-details-best-selling">
<div>
<Button icon="isax isax-close-circle" severity="secondary" rounded link @click="dialog.close()" />
<h2>{{ title }}</h2>
<img :src="coverImage">
</div>
<ul>
<li v-for="(value, key ) in items" :key="key">
<label>{{ $t(key) }}</label>
<span>{{ value }}</span>
</li>
</ul>
</div>
</template>
<script setup>
const dialog = inject('dialogRef')
const { product, quantity, total } = dialog.value.data
const { title, coverImagem, stockQuantity} = product
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
const items = reactive({
salesNumber: quantity,
salesPrice: numberFormat(total),
inventory: stockQuantity,
status: "دارای رقیب",
})
</script>
<style lang="scss">
.seller-details-best-selling {
@apply flex flex-col bg-white rounded-t-[0.6rem] gap-[1.9rem] px-6 py-[1.1rem] rtl;
&>div {
@apply flex gap-1.5;
h2 {
@apply w-56 text-[#333333] text-sm font-medium font-vazir pt-0.5;
}
button {
@apply -mx-4 -my-3;
.p-button-icon {
@apply text-lg text-[#B2B2B2] #{!important};
}
}
img {
@apply w-[3.8rem] h-[3.8rem] mr-auto;
}
}
&>ul {
@apply flex flex-col gap-[1.1rem] mt-1.5;
li {
@apply flex justify-between;
label {
@apply text-[#5D5D5D] text-xs font-vazir
}
span {
@apply text-[#333333] text-xs font-medium font-vazir;
}
}
}
}
</style>
@@ -0,0 +1,98 @@
<template>
<div class="seller-details-order">
<div>
<Button icon="isax isax-close-circle" severity="secondary" rounded link @click="dialog.close()" />
<h2>{{ $t('orderDetails') }}</h2>
<Button icon="isax isax-receipt-item" iconPos="right" link :label="$t('showFactor')" @click="show()" />
</div>
<ul>
<li v-for="(value, key ) in items" :key="key">
<label>{{ $t(key) }}</label>
<span>{{ value }}</span>
</li>
</ul>
<ul>
<li v-for="(product, k) in [product]" :key="k">
<span v-if="product.quantity > 1">
{{ product.quantity }}x
</span>
<img :src="product.coverImage">
</li>
</ul>
</div>
</template>
<script setup>
const dialog = inject('dialogRef')
const { total, product} = dialog.value.data
const items = reactive({
address: ".........................................................................",
receiver: "علی مصلحی",
purchaseDate: "1402/05/13",
postageDate: "1402/05/20",
price: "252,000",
})
</script>
<style lang="scss">
.seller-details-order {
@apply flex flex-col bg-white rounded-t-[0.6rem] gap-[1.1rem] px-6 pt-[1.1rem] rtl;
&>div {
@apply flex items-center gap-1.5;
h2 {
@apply text-[#333333] text-sm font-medium font-vazir;
}
&>button {
@apply -mx-4 -my-3 last:mr-auto;
&:first-child {
@apply text-lg text-[#B2B2B2] #{!important};
}
&:last-child{
@apply text-[#47B556] font-vazir;
.p-button-label{
@apply text-xs;
}
}
}
}
&>ul:first-of-type {
@apply flex flex-col gap-[1.1rem] mt-1.5;
li {
@apply flex justify-between;
label {
@apply text-[#5D5D5D] text-xs font-vazir
}
span {
@apply text-[#333333] text-xs font-medium font-vazir;
}
}
}
&>ul:last-of-type {
@apply flex items-center gap-[1.1rem] pb-[1.1rem] border-t border-[#E5E5E5];
li {
@apply flex flex-col h-28 items-center justify-end gap-2.5;
span {
@apply text-[#333333] text-xs font-normal font-poppins
}
img {
@apply w-[3.8rem] h-[3.8rem];
}
}
}
}
</style>
@@ -0,0 +1,62 @@
<template>
<div class="seller-details-product">
<div>
<h2>{{ title }}</h2>
<Button icon="isax isax-close-circle" severity="secondary" rounded link @click="dialog.close()" />
</div>
<ul>
<li v-for="(value, key ) in items" :key="key">
<label>{{ $t(key) }}</label>
<span>{{ value }}</span>
</li>
</ul>
</div>
</template>
<script setup>
const { title = 'هارددیسک اکسترنال ای دیتا مدل HM900 ظرفیت 4 ترابایت' } = reactive({})
const items = reactive({
salesPrice: "242,523",
inventory: "1",
status: "دارای رقیب",
})
</script>
<style lang="scss">
.seller-details-product {
@apply flex flex-col gap-[1.9rem] px-6 py-[1.1rem] rtl;
&>div {
@apply flex items-center justify-between gap-1.5;
h2 {
@apply w-56 text-[#333333] text-sm font-medium font-vazir;
}
button {
@apply -mx-4 -my-3;
.p-button-icon {
@apply text-lg text-[#B2B2B2] #{!important};
}
}
}
&>ul {
@apply felx flex-col gap-[1.1rem] mt-1.5;
li {
@apply flex justify-between;
label {
@apply text-[#5D5D5D] text-xs font-vazir
}
span {
@apply text-[#333333] text-xs font-medium font-vazir;
}
}
}
}
</style>
+201
View File
@@ -0,0 +1,201 @@
<template>
<section class="seller-first-step">
<ul>
<li v-for="({ is, props, label }, key) in fields.first" :key="key">
<Editor v-if="key == 'description'" v-bind="props" v-model="form[key]"/>
<component v-else :is="is" v-bind="props" v-model="form[key]" />
<label :for="key"> {{ label }} </label>
<small v-if="errors[key]">{{ errors[key] }}</small>
</li>
</ul>
<div>
<Button v-bind="btns.next.props" @click="next()" />
</div>
</section>
</template>
<script setup>
import Editor from 'primevue/editor';
const { t } = inject('service')
const { fields, btns } = inject('init')
const form = inject('form')
const step = inject('step')
const { brands } = inject('data')
const errors = ref({})
fields.first.brandID.props.options = brands
const categories = useCategoriesStore()
fields.first.subCategory.props.options = computed(() => {
let items = []
categories.items.forEach((item) => {
if (item.sub) {
item.sub.forEach((item2) => {
if (item2._id == form.category) {
items = item2.sub ?? [item2]
}
})
}
})
return items;
})
fields.first.minBuy.props.disabled = computed(() => !form.boxSeller)
fields.first.maxBuy.props.disabled = computed(() => !form.boxSeller)
fields.first.itemInBox.props.disabled = computed(() => !form.boxSeller)
if (form.boxSeller) {
const { min = 1, max = 20 } = form?.orderRange
form.minBuy = min
form.maxBuy = max
}
watch(
computed(() => Object.assign({}, form)),
(value, old) => {
Object.keys(errors.value).forEach((key) => {
if (value[key] != old[key])
delete errors.value[key]
})
},
{ deep: true })
const next = () => {
errors.value = {}
Object.keys(fields.first).forEach((key) => {
if (!['localSend', 'boxSeller', 'warranty'].includes(key) && !fields.first[key].props.disabled) {
if ([undefined, null, ''].includes(form[key])) {
errors.value[key] = t('required')
}
}
})
if (Object.keys(errors.value).length < 1)
step.value++;
}
</script>
<style lang="scss">
.seller-first-step {
@apply w-full flex flex-col pb-20 gap-8 lg:gap-[3.8rem];
&>ul {
@apply grid grid-cols-2 lg:grid-cols-4 gap-x-3 gap-y-6 lg:gap-x-6 lg:gap-y-10 w-full max-w-full;
li {
@apply w-full h-14 relative max-xl: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-sm lg: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;
}
&: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;
}
}
.p-dropdown,
.p-inputnumber {
@apply w-full;
input {
@apply ltr text-left;
}
}
.p-dropdown-label {
@apply font-vazir pt-4;
}
&:nth-of-type(2) {
label {
@apply left-3 right-auto;
}
}
&:nth-of-type(1),
&:nth-of-type(2) {
@apply col-span-2;
}
&:nth-of-type(11) {
@apply col-span-2 lg:col-span-3 row-span-3 h-full max-lg:order-last;
textarea {
@apply grow h-full;
}
}
&:has(.p-checkbox) {
@apply border border-[#CCCCCC] rounded-[0.6rem] flex flex-row-reverse justify-between items-center px-4;
label {
@apply static text-sm lg:text-base text-neutral-400 h-max pointer-events-auto cursor-pointer #{!important};
}
.p-checkbox {
@apply w-[1.1rem] lg:w-5 shrink-0;
}
}
&:nth-of-type(9),
&:nth-of-type(10),
&:nth-of-type(12),
&:nth-of-type(13) {
@apply max-lg:col-span-1;
}
small {
@apply text-red-500 font-vazir;
}
&:has(small) * {
@apply border-red-500 #{!important};
}
&:has(.p-overlay-open)::after {
@apply content-['_'] inset-0 fixed bg-black/20 z-[1];
}
&:has(.p-disabled),
&:has([disabled]) {
@apply max-lg:hidden lg:invisible;
}
.p-editor-content{
@apply rtl text-right;
}
}
}
&>div {
@apply inset-x-0 bottom-0 flex gap-3 w-full lg:w-max 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:w-[11.4rem] lg:h-14 #{!important};
.p-button-label {
@apply text-sm lg:text-xl font-medium font-vazir;
}
}
}
}
</style>
+122
View File
@@ -0,0 +1,122 @@
<template>
<section class="seller-second-step">
<ul>
<li v-for="(_, key) in form.specs" :key="key">
<IconField iconPosition="left">
<InputText :id="key" placeholder v-model="form.specs[key]" />
<InputIcon class="isax isax-close-circle" @click="delete form.specs[key]" />
</IconField>
<label :for="key"> {{ key }} </label>
<small v-if="errors[key]">{{ errors[key] }}</small>
</li>
<li @click="handleClick()" v-ripple>
<span>{{ $t('addSpec') }}</span>
<i class="isax isax-box-add" />
</li>
</ul>
<div>
<Button v-bind="btns.prev.props" @click="step--" />
<Button v-bind="btns.next.props" :disabled="disabled" @click="next()" />
</div>
</section>
</template>
<script setup>
const { dialog, t } = inject('service')
const { btns } = inject('init')
const form = inject('form')
const step = inject('step')
const disabled = computed(() => form.specs.length > 0)
const errors = ref({})
const next = () => {
errors.value = {}
Object.entries(form.specs).forEach(([key, value]) => {
if ([undefined, null, ''].includes(value))
errors.value[key] = t('required')
})
if (Object.keys(errors.value).length < 1)
step.value++;
}
const handleClick = () => {
dialog.show(resolveComponent('PanelSellerDialogAddSpec'), form)
}
</script>
<style lang="scss">
.seller-second-step {
@apply w-full flex flex-col gap-8 lg:gap-[3.8rem];
ul {
@apply grid lg:grid-cols-3 gap-x-3 gap-y-6 lg:gap-x-6 lg:gap-y-10 w-full max-w-full;
li {
@apply w-full h-14 relative;
label {
@apply w-max absolute right-3 max-lg:translate-y-0.5 top-4 px-1 bg-white transition-['top'] select-none;
@apply text-neutral-400 text-sm lg:text-base font-medium font-vazir cursor-text pointer-events-none;
}
input {
@apply w-full rounded-[0.6rem] border shadow-none border-[#CCCCCC] text-zinc-800 font-vazir rtl;
}
.p-input-icon {
@apply text-lg lg:text-xl h-max -translate-y-[20%] cursor-pointer;
}
&:has(input:focus),
&:has(input:not(:placeholder-shown)) {
label {
@apply -top-2 h-4;
@apply text-zinc-800 text-xs font-normal;
}
}
.p-inputnumber {
@apply w-full;
}
&:last-child {
@apply cursor-pointer border border-[#CCCCCC] rounded-[0.6rem] px-4 pointer-events-auto;
@apply flex justify-between items-center overflow-hidden relative;
span {
@apply text-neutral-400 text-sm lg:text-base font-medium font-vazir;
}
i {
@apply text-[#47B556] text-2xl;
}
}
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-max 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:w-[11.4rem] lg:h-14 #{!important};
.p-button-label {
@apply text-sm lg:text-xl font-medium font-vazir;
}
}
}
}
</style>
+171
View File
@@ -0,0 +1,171 @@
<template>
<section class="seller-thirdy-step">
<ul>
<li>
<UploadImage v-model="form.coverImage" cover @update:modelValue="delete errors.coverImage" />
<small v-if="errors.coverImage">{{ errors.coverImage }}</small>
</li>
<li v-for="(_, i) in form.images" :key="i">
<UploadImage v-model="form.images[i]" @update:modelValue="update" />
<small v-if="errors.images && i < 1">{{ errors.images }}</small>
</li>
</ul>
<div>
<Button v-bind="btns.prev.props" @click="step--" />
<Button v-bind="btns.submit.props" :loading="loading" @click="submit()" />
</div>
</section>
</template>
<script setup>
import UploadImage from '../َUploadImage.vue'
const { btns } = inject('init')
const { toast, t } = inject('service')
const router = useRouter()
const form = inject('form')
const step = inject('step')
const loading = ref(false)
const errors = ref({})
const update = () => {
form.images = form.images.filter((image) => image != null)
form.images.push(null)
delete errors.value.images;
}
const submit = async () => {
errors.value = {}
const images = form.images.filter((image) => image != null)
if (!form.coverImage)
errors.value.coverImage = t('required')
if (images.length < 1)
errors.value.images = t('required')
if (Object.keys(errors.value).length < 1) {
loading.value = true
const body = Object.assign({}, form)
body.images = body.images.filter((image) => image)
const { minBuy: min, maxBuy: max } = body
body.orderRange = { min, max }
delete body.minBuy
delete body.maxBuy
//Upload File
if (typeof body.coverImage === 'object') {
const formData = new FormData()
formData.append('images', form.coverImage)
const { status, data, error } = await useFetch('/api/images/upload', {
headers: { Authorization: useCookie("token") }, params: { type: 1 }, body: formData, method: 'post'
})
if (status.value == 'success')
body.coverImage = data.value.urls[0]
else {
loading.value = false
return toast.add({
life: 2000, severity: 'error', summary: t('error'), detail: error.value.msg
})
}
}
//upload images
const files = body.images.filter((image) => typeof image === 'object')
if (files.length > 0) {
const formData = new FormData()
files.forEach((image, i) => {
formData.append('images', image)
});
const { status, data, error } = await useFetch('/api/images/upload', {
headers: { Authorization: useCookie("token") }, params: { type: 1 }, body: formData, method: 'post'
})
if (status.value == 'success') {
body.images = body.images.map((image) => {
if (typeof image === 'object')
return data.value.urls.shift();
return image;
})
} else {
loading.value = false
return toast.add({
life: 2000, severity: 'error', summary: t('error'), detail: error.value.msg
})
}
}
let endPoint = '/api/products'
let method = 'post'
if (body.id) {
endPoint += '/' + body.id
method = 'put'
}
const { status, data, error } = await useFetch(endPoint, {
headers: { Authorization: useCookie("token") }, method, body
})
if (status.value == 'success')
router.push('/panel/seller/products')
else if (error.value.statusCode == 422)
errors.value = error.value.data
else toast.add({
life: 3000, severity: 'error', summary: t('error'), detail: error.value.message
})
loading.value = false
}
}
onMounted(() => {
if (form.id) update()
})
</script>
<style lang="scss">
.seller-thirdy-step {
@apply w-full flex flex-col gap-8 pb-20 lg:gap-[3.8rem];
&>ul {
@apply flex flex-wrap gap-6;
li {
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>
+82
View File
@@ -0,0 +1,82 @@
<template>
<div class="seller-upload-image">
<div v-if="!source" @drop.prevent="chooseImage('dataTransfer', $event)" @dragover.prevent>
<i class="isax isax-gallery-export"></i>
<p>{{ 'cover' in $attrs ? $t('coverImage') : '&nbsp;' }}</p>
<h2>{{ $t('forUploadImages') }} </h2>
<span @click="showImages()">{{ $t('clickHere') }}</span>
</div>
<div v-else>
<img :src="source" />
<Button :label="$t('placement')" severity="secondary" text raised @click="showImages()" />
<Button :label="$t('delete')" severity="secondary" text raised @click="deleteImage()" />
</div>
<input ref="input" type="file" hidden @input="chooseImage('target', $event)">
</div>
</template>
<script setup>
const image = defineModel()
const input = ref()
const source = ref()
const showImages = () => input.value.click();
const deleteImage = () => {
image.value = null
source.value = null
input.value.value = null
}
const chooseImage = (name, e) => {
const binaryData = [];
binaryData.push(e[name].files[0]);
source.value = URL.createObjectURL(new Blob(binaryData, { type: "image" }))
image.value = e[name].files[0];
}
onMounted(() => {
if(image.value) source.value = image.value
})
</script>
<style lang="scss">
.seller-upload-image {
@apply w-[21.4rem] h-[21.4rem] lg:w-[24.8rem] lg:h-[24.8rem] bg-white;
&>div:not(:has(img)) {
@apply h-full flex flex-col justify-center items-center border border-[#47B556] border-dashed rounded-[0.6rem];
i {
@apply text-[3.1em] lg:text-[3.8em] text-[#47B556];
}
p {
@apply text-[#7F7F7F] text-[0.6em] lg:text-sm font-medium font-vazir mt-6;
}
h2 {
@apply text-[#333333] text-sm lg:text-lg font-medium font-vazir mt-1.5;
}
span {
@apply text-[#47B556] text-sm lg:text-lg font-medium font-vazir cursor-pointer;
}
}
&>div:has(img) {
@apply h-full flex flex-col gap-3 justify-center items-center relative;
img {
@apply h-full w-full absolute object-cover rounded-[0.6rem];
}
button {
@apply w-[7.5rem] h-[3.1rem] bg-white z-[1];
}
&::after{
@apply bg-[#47B556]/20 absolute content-['_'] inset-0 rounded-[0.6rem];
}
}
}
</style>
+89
View File
@@ -0,0 +1,89 @@
<template>
<div class="buy">
<p>
<span>{{ $t('priceForYou') }}</span>
<small v-if="boxSeller">
{{ $t('everyBox') }}
</small>
</p>
<div>
<span v-if="specialPrice">
{{ numberFormat(specialPrice) }}
<small>{{ $t('priceUnit') }}</small>
</span>
<span>
{{ numberFormat(price) }}
<small v-if="!specialPrice">
{{ $t('priceUnit') }}
</small>
</span>
</div>
<CartQuantity v-if="cart.find(id)" v-model="quantity" :range="orderRange" />
<Button v-else :label="$t('addToCart')" :loading="cart.loading" @click="quantity = orderRange.min" />
</div>
</template>
<script setup>
const { id, orderRange, boxSeller, specialPrice, price } = inject('data')
const cart = useCartStore()
const quantity = computed({
get: () => cart.find(id)?.quantity || 0,
set: (v) => cart.update(id, v)
})
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.product .buy {
@apply flex flex-wrap justify-between items-center px-6 pt-1 pb-2.5 lg:gap-6 shadow-[0px_-3px_5px_#00000005];
@apply fixed inset-x-0 bottom-0 h-[6.2rem] bg-white shadow border-t border-[#F1F2F4] lg:h-auto;
@apply z-10 lg:z-0 lg:static lg:bg-transparent lg:shadow-none lg:border-t-0 lg:p-0 lg:gap-y-6 lg:mt-auto;
p {
@apply w-full lg:w-max text-[#7F7F7F] font-medium font-vazir pr-1.5 lg:p-0;
span {
@apply text-xs lg:text-xl leading-[2.1rem];
}
small {
@apply text-[0.7em] lg:text-base leading-[1.9rem];
}
}
&>div:nth-of-type(1) {
@apply flex flex-col justify-end gap-2 max-lg:order-last;
span {
@apply text-left text-[#333] text-sm font-bold lg:text-[1.6em] font-vazir;
}
small {
@apply text-xs font-medium lg:hidden;
}
&:has(span:nth-child(2)) {
span:first-child {
@apply max-lg:-mt-8;
}
span:last-child {
@apply line-through text-xs lg:text-base text-gray-600;
}
}
}
&>button {
@apply w-[10.8rem] h-[2.6rem] lg:w-[25.1rem] lg:h-14 #{!important};
.p-button-label {
@apply text-xs lg:text-xl font-bold font-iran-sans;
}
}
}
</style>
+169
View File
@@ -0,0 +1,169 @@
<template>
<router-link :to="`/product/${uid}`">
<article :class="['product-card', `product-card-${type}`]">
<img :src="coverPath" :alt="title" />
<div>
<h2 v-if="type != 'minimal'">
{{ category[category.length - 1]?.name }}
</h2>
<h1>{{ title }}</h1>
<ul v-if="type != 'minimal' && stars">
<li v-for="i in 5" :key="i">
<img :src="`/icons/star.svg`" :class="starsClass(i)" />
</li>
</ul>
<template v-if="type != 'minimal'">
<Button :icon="`isax isax-heart${like ? '5' : ''}`" severity="secondary" text
@click.prevent="favorite.update(id)" />
</template>
<div v-if="type != 'minimal'">
<div>
<span>
<b>{{ numberFormat(price) }}</b>
<small>{{ $t('priceUnit') }}</small>
</span>
<s v-if="type == 'action' && specialPrice">
{{ numberFormat(specialPrice) }}
</s>
</div>
<ProductQuantity v-if="type == 'action'" v-model="quantity" :range="orderRange" />
</div>
</div>
</article>
</router-link>
</template>
<script setup>
const props = defineProps({
data: { type: Object, default: {} },
type: { type: String, default: 'main' }
})
const { _id: id, uid, coverPath, category = [], title, price, specialPrice, stars, orderRange } = props.data
const favorite = useFavoritesStore()
const like = computed(() => favorite.find(id))
const cart = useCartStore()
const quantity = computed({
get: () => cart.find(id)?.quantity || 0,
set: (v) => cart.update(id, v)
})
const starsClass = (i) => (i > stars ? 'opacity-25' : '')
const numberFormat = (number) =>
new Intl.NumberFormat('fa-IR', { maximumSignificantDigits: 3 }).format(number)
</script>
<style lang="scss">
.product-card {
box-shadow: 0px 2px 10px 0px #87C29A;
@apply bg-white rounded-[0.6rem] border flex flex-col relative;
&>img {
@apply w-[3.8rem] h-[3.8rem] lg:w-[10.3rem] lg:h-[10.3rem] self-center object-contain object-bottom;
}
&>div {
@apply flex flex-col grow;
h2 {
@apply text-[#7F7F7F] text-[0.6em] leading-5 lg:text-sm font-medium font-vazir;
}
h1 {
@apply text-[#333] text-[0.7em] leading-5 lg:text-base font-bold font-vazir mt-1 line-clamp-1 lg:line-clamp-2;
}
ul {
@apply hidden lg:flex gap-1 mt-3 ;
img {
@apply w-4 h-4;
}
}
&>div {
@apply flex justify-between items-center gap-4 mt-auto;
&>div:nth-of-type(1) {
@apply flex flex-col items-end font-vazir;
span {
@apply text-[#333];
small {
@apply text-[0.6em] lg:text-[0.8em] font-medium mr-1;
}
b {
@apply text-sm lg:text-lg;
}
}
s {
@apply mt-1 text-[#7F7F7F] text-[0.7em] lg:text-[0.9em] font-medium;
}
}
}
&>button {
@apply absolute rounded-full w-12 h-12 #{!important};
.p-button-icon {
@apply text-base lg:text-2xl text-[#333];
}
}
}
}
.product-card-main {
@apply w-[9.3rem] h-[11.4rem] p-3 lg:w-[18.25rem] lg:h-[25.4rem] lg:p-6 lg:pb-8;
&>div {
h2 {
@apply mt-6;
}
&>button {
@apply absolute -bottom-1 -left-1 lg:bottom-[1.4rem] lg:left-3.5;
}
}
}
.product-card-action {
@apply w-[12.6rem] h-[16.5rem] lg:w-[18.3rem] lg:h-[29rem] p-3 lg:p-5;
&>img {
@apply max-lg:mt-1.5 max-lg:w-[5.7rem] max-lg:h-[5.7rem];
}
&>div {
h2 {
@apply mt-9;
}
&>div {
@apply flex-row-reverse;
}
&>button {
@apply absolute top-1 left-1 lg:top-2 lg:left-2;
}
}
}
.product-card-minimal {
@apply w-[10.1rem] h-[11.4rem] px-2.5 gap-[1.9rem] items-center;
@apply lg:w-[16.5rem] lg:h-[16.5rem] py-6 lg:p-4 lg:gap-6;
&>div {
h1 {
@apply text-center text-[0.7em] lg:text-base leading-4;
}
}
img {
@apply w-[4.7rem] h-20 lg:w-[8.4rem] lg:h-[8.9rem];
}
}
</style>
+49
View File
@@ -0,0 +1,49 @@
<template>
<section class="description">
<h2>{{ $t('description') }}</h2>
<div>
<div v-html="description" />
</div>
</section>
</template>
<script setup>
const { description } = inject('data')
</script>
<style lang="scss">
.product .description {
@apply w-full flex items-start gap-[13.8rem] px-6;
& > h2 {
@apply hidden lg:flex gap-3 items-center text-primary-600 text-xl font-bold font-vazir;
&::before {
@apply content-['_'] h-6 w-[0.3rem] rounded-full bg-primary-600;
}
}
& > div {
@apply w-full max-h-[17.1rem] lg:w-[57.8rem] lg:max-h-[36.1rem] overflow-y-auto lg:pt-6;
&::-webkit-scrollbar-track {
@apply bg-transparent;
}
&::-webkit-scrollbar {
@apply w-1;
}
&::-webkit-scrollbar-thumb {
@apply bg-[#E5E5E5];
}
div * {
@apply font-vazir;
all: initial;
// @apply text-[#595959] text-xs lg:text-xl font-normal font-vazir lg:leading-9 pl-4 lg:pl-8;
}
}
}
</style>
+78
View File
@@ -0,0 +1,78 @@
<template>
<div class="gallery">
<div>
<ul>
<li v-for="(act, key) in actions" :key="key">
<Button v-bind="act.props" @click="act.click()" />
</li>
</ul>
<img :src="coverImage" />
</div>
<ul>
<li v-for="(image, i) in images" :key="i">
<i class="isax isax-gallery-slash" />
<Image :src="image" preview />
</li>
</ul>
</div>
</template>
<script setup>
const { actions } = inject('init')
const { coverImage, images } = inject('data')
</script>
<style lang="scss">
.product .gallery {
@apply w-full h-full max-h-[38.4rem] flex flex-col gap-6 ;
&>div {
@apply flex p-6 w-full grow max-h-[29.9rem] border border-[#E5E5E5] overflow-hidden;
&>ul {
@apply flex flex-col gap-0 -m-3 relative;
li {
@apply ltr;
i {
@apply absolute inset-0;
}
button {
@apply w-12 h-12 #{!important};
span {
@apply text-2xl text-[#333333];
}
}
}
}
&>img {
@apply flex w-full h-full max-w-[24.1rem] max-h-[24.1rem] object-contain;
@apply shrink mt-5 mx-auto 2xl:translate-x-3 px-8 object-top;
}
}
&>ul {
@apply flex gap-3 flex-wrap h-28 overflow-hidden;
li {
@apply w-28 h-28 flex items-center justify-center relative bg-[#F2F2F2] cursor-pointer;
span {
@apply w-full h-full absolute;
img {
@apply w-full h-full object-cover;
}
}
i {
@apply text-2xl absolute;
}
}
}
}
</style>
+59
View File
@@ -0,0 +1,59 @@
<template>
<div class="product-card-quantity">
<p v-if="count < 0">{{ $t('unavailable') }}</p>
<template v-else-if="count == 0">
<Button :label="$t('buy')" icon="isax isax-bag-2" iconPos="right" text @click.prevent="count++" />
</template>
<template v-else>
<Button text icon="isax isax-add" @click.prevent="count++" :disabled="count >= range.max" />
<p>{{ count }}</p>
<Button text icon="isax isax-minus" @click.prevent="count--" :disabled="count <= range.min" />
</template>
</div>
</template>
<script setup>
defineProps({
range: { type: Object, default: { min: 1, max: 1000 } }
})
const count = defineModel()
</script>
<style lang="scss">
.product-card-quantity {
@apply flex items-center justify-between w-[4.8rem] h-[1.9rem] lg:w-[5.8rem] lg:h-10 rounded-[1.8rem] border border-primary-600 overflow-hidden;
p {
@apply flex w-max items-center text-primary-600 text-[0.7em] lg:text-[1.1em] font-medium font-vazir mx-auto;
}
&:not(:has(button)) {
@apply border-[#7F7F7F];
p {
@apply text-[#7F7F7F];
}
}
button {
@apply rounded-full font-normal font-iran-sans text-[0.7em] lg:text-[1.1em] #{!important};
.isax {
@apply text-xl;
}
}
button:not(:has(.isax-bag-2)) {
@apply w-7 h-7 first:mr-1 last:ml-1 #{!important};
}
button:has(.isax-bag-2) {
@apply w-full h-full #{!important};
}
button[disabled]{
@apply opacity-20;
}
}
</style>
+204
View File
@@ -0,0 +1,204 @@
<template>
<div class="seller">
<div>
<h2>{{ $t('seller') }}</h2>
<template v-if="device == 'mobile'">
<Button :label="$t('otherSellersCount', { count: 3 })" text @click="handleClick" />
</template>
</div>
<ul>
<li v-for="(item, i) in items.slice(0, 3)" :key="i">
<i :class="['isax', `isax-${item.icon}`]" />
<span>{{ item.title }}</span>
</li>
<li v-show="shopID?.rate >= 0">
<small>{{ shopID.rate * 10 }}%</small>
<span>{{ $t('satisfaction') }}</span>
<span>{{ $t('performance') }} {{ shopID?.performance }}</span>
</li>
<li v-show="boxSeller">
<i class="isax isax-d-square"></i>
<span>{{ $t('boxShopping') }}</span>
<span>{{ $t('minBuyBox', { minBuy }) }}</span>
</li>
<li v-show="boxSeller">
<div><i /></div>
<span>{{ $t('itemInBox', { itemInBox }) }}</span>
</li>
<li>
<div>
<template v-for="({ primary }, i) in items.slice(3)" :key="i">
<i :class="primary ? 'bg-primary-600' : 'bg-neutral-200'" />
</template>
</div>
<ul>
<li v-for="(item, i) in items.slice(3)" :key="i">
<i :class="`isax isax-${item.icon}`" />
<span>{{ item.title }}</span>
</li>
</ul>
</li>
</ul>
<ProductBuy />
</div>
</template>
<script setup>
const { dialog, device, t } = inject('service')
const { shopID, warranty, boxSeller, localSend, minBuy, itemInBox } = inject('data')
const items = reactive([
{ title: shopID.shopName, icon: 'shop' },
{ title: warranty, icon: 'shield-tick' },
{ title: t('availableSellerWarehouse'), icon: 'box-tick' },
{ title: t('sendFromLocal'), icon: 'box', primary: localSend },
{ title: t('sendFromSeller'), icon: 'profile-circle', primary: !localSend }
])
const handleClick = () => {
dialog.show(resolveComponent('ProductOtherSellers'))
}
</script>
<style lang="scss">
.product .seller {
@apply w-full lg:w-[28.1rem] lg:h-[42rem] bg-[#FAFAFA] lg:rounded-[0.6rem] lg:border border-neutral-200;
@apply flex flex-col gap-6 lg:gap-7 px-6 py-5 lg:py-6 max-lg:order-4;
&>div:not([class]) {
@apply flex justify-between items-center;
h2 {
@apply text-[#191919] text-base lg:text-2xl font-bold font-vazir;
}
button {
@apply rtl p-0 h-max #{!important};
.p-button-label {
@apply text-[0.7em] text-primary-600;
}
}
}
&>ul {
@apply flex flex-col gap-[1.1rem] lg:gap-4;
&>li {
@apply flex items-center;
&:nth-of-type(-n + 3) {
@apply gap-3 lg:gap-4;
i {
@apply text-[1.4em] lg:text-2xl;
}
span {
@apply text-[#333] text-sm lg:text-xl font-medium font-vazir;
}
}
&:nth-of-type(2) {
@apply order-5 py-[1.2rem] lg:py-4 border-y border-[#E5E5E5];
}
&:nth-of-type(3) {
@apply order-6;
}
&:nth-of-type(4) {
@apply gap-1 lg:pr-10 font-vazir order-2 pb-[1.1rem] lg:pb-4 border-b border-[#E5E5E5];
small {
@apply text-[#019907] text-xs lg:text-base;
}
span {
@apply text-[#999999] text-xs lg:text-base;
&:first-of-type {
@apply pl-2.5 relative;
&::after {
@apply content-['_'] w-px bg-[#D9D9D9] h-4 absolute left-0.5 top-1/2 -translate-y-1/2;
}
}
}
}
&:nth-of-type(5) {
@apply gap-4 font-vazir order-3;
i {
@apply text-[#019907] text-[1.4em] lg:text-2xl;
}
span {
@apply text-[#019907] text-xs lg:text-lg font-medium;
&:first-of-type {
@apply pl-2.5 -ml-2 relative;
&::after {
@apply content-['_'] w-px bg-[#D9D9D9] h-4 absolute left-0 top-1/2 -translate-y-1/2;
}
}
}
}
&:nth-of-type(6) {
@apply gap-4 font-vazir order-4;
div {
@apply w-2 lg:w-2.5 h-5 lg:h-6 mx-[0.4rem] -mt-2.5 lg:-mt-3.5 relative flex justify-center;
i {
@apply w-2 h-2 lg:w-2.5 lg:h-2.5 rounded-full mt-auto bg-[#019907] z-0;
}
&::before {
@apply content-['_'] absolute w-0.5 inset-y-0 bg-neutral-200;
}
}
span {
@apply text-[#019907] text-xs lg:text-lg font-medium;
}
}
&:nth-of-type(7) {
@apply gap-6 lg:gap-4 order-7 pb-4 lg:border-b border-[#E5E5E5];
div {
@apply w-2.5 h-[4.3rem] lg:mx-[0.4rem] pt-4 lg:pt-3.5 -mt-2 lg:-mt-3.5 flex flex-col items-center justify-between relative;
i {
@apply w-2.5 h-2.5 rounded-full z-0;
}
&::before {
@apply content-['_'] absolute w-0.5 inset-y-0 bg-neutral-200;
}
}
ul {
@apply flex flex-col mt-2 gap-6 lg:gap-5 font-vazir;
li {
@apply flex gap-2 items-center h-4 lg:h-[1.6rem];
i {
@apply text-xl text-primary-600;
}
span {
@apply text-[#999] text-[0.7em] lg:text-base font-normal;
}
}
}
}
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More