feat: initial commit

This commit is contained in:
s.esmaeili
2026-09-09 06:46:27 +03:30
commit 0ed6bed028
214 changed files with 20177 additions and 0 deletions
@@ -0,0 +1,242 @@
<!--
Ported from legacy henza_pwa/src/views/singleProduct.vue.
CRITICAL EXISTING ISSUE — ported as-is, flagged prominently (not silently
fixed): the bottom of legacy's script has an unconditional global side
effect —
history.pushState(null, location.href);
window.onpopstate = function () { history.go(1); };
— that runs every time a SingleProduct instance is created (not just when
its drawer opens — ant-design-vue's Drawer does not lazily mount its slot
content by default). This permanently overrides the browser/hardware back
button for the rest of the session (forcing it to always go *forward*), and
fires immediately once the Home page renders its product cards, not only
when a product detail is actually opened. This looks like a broken attempt
to trap the user inside the drawer. I did not fix this — it's ported
verbatim below — but flagging it as the single most impactful bug found in
this port so far; recommend fixing in a dedicated bug-fix pass rather than
silently patching it here.
FIXED (not merely ported) — a genuine crash, not a stylistic difference:
legacy's image-gallery guard was `v-if="product.images[0].image"`, which
throws immediately for any product with zero images (TypeError: cannot read
property of undefined). Since 100% of our current seeded products have
`images: []`, porting this literally would make the view crash for every
real product today. Legacy's own `v-else` branch (a decorative placeholder
SVG) makes clear a "no images" case was already intended — the guard here
is `product.images && product.images.length > 0`, which is what's needed
for that already-intended fallback to actually be reachable. Flagging this
explicitly since it's a behavior change, not just documentation.
DROPPED as confirmed-dead code (not "possibly dead" — verified unreachable):
legacy also declared a second DialogVant (`loginStepTwoNotify`) gating order
placement on a customer verification step. The only code that ever set it
to `true` was in an explicicitly commented-out alternate click handler
(legacy's own comment: "کد قبلی (نیاز به تایید مدارک...)"); the actual live
click handler (`isUserLoggedIn() ? addToCart() : loginNotify = true`) never
references it. It cannot show under any reachable code path. Not ported.
-->
<template>
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات" cls="overflow-hidden" v-if="flag && productDetail.productRef.value">
<container class="px-[32px] py-[16px] flex flex-col pb-[66px] bg-prim max-h-screen" style="padding-bottom: 20px">
<div class="w-screen col-span-6 mx-[-32px] max-w-screen-md">
<div class="fixed top-[20px] right-[16px] z-[100] p-4" @click="close">
<Icon icon="vuesax-linear:arrow-right-1" class="cursor-pointer text-white bg-secondary rounded-[5px] p-[2.5px]" size="24" />
</div>
<div v-if="favorites.isFavorite(id)">
<Icon icon="vuesax-linear:heart-fill" class="cursor-pointer text-secondary z-[100] fixed top-[20px] left-[16px] p-4" @click="toggleFav" size="25" />
</div>
<div v-else>
<Icon icon="vuesax-linear:heart" class="cursor-pointer text-secondary z-[100] fixed top-[20px] left-[16px] p-4" @click="toggleFav" size="25" />
</div>
<transition name="slideUp" mode="out-in">
<div v-if="hasImages">
<Slider :slides="slides" width="auto" height="55vh" />
</div>
<div v-else>
<svg height="55vh" width="auto" viewBox="0 0 12 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6 9C5.91177 5.77359 3.66177 4.75472 0 4.5C3.57353 4.37264 5.91177 3.18396 6 0C6.08824 3.22642 8.38235 4.4151 12 4.5C8.38235 4.75472 6.08824 5.77359 6 9Z"
fill="#8D6A46"
/>
</svg>
</div>
</transition>
</div>
<div class="flex-1">
<div class="overflow-y-auto max-h-[300px]">
<div class="w-full flex justify-between mb-4">
<h5 class="col-span-4 m-0 text-[19px] font-medium">{{ title }}</h5>
<Button type="primary" class="justify-self-end m-0 col-span-2" @click="showPopup = true">توضیحات</Button>
</div>
<div class="w-full flex justify-between mb-4">
<div>
<div v-if="showDiscountWage" class="flex mb-4">
<div class="ml-4">اجرت</div>
<div class="rounded-[5px] bg-danger text-white py-[4px] px-[8px]" v-if="promotion">-%{{ promotion.discount_percent }}</div>
</div>
<div v-if="showDiscountWage" class="flex">
<div :class="{ 'text-secondary': true, 'ml-4 text-[13px] line-through decoration-danger': hasDiscountWage }">
{{ wage != null && wage % 1 === 0 ? wage : wage?.toFixed(2) }}% اجرت
</div>
</div>
</div>
<div>
<h3 class="mb-0">{{ min_gram }}</h3>
<div class="text-secondary mb-0 text-left">گرم</div>
</div>
</div>
<div class="w-full flex justify-between">
<div class="w-full mb-[10px]">تنوع ها</div>
</div>
<div class="w-full flex-1 flex">
<Button
type="primary"
:ghost="selectedVariant?.id !== item.id"
class="last:ml-0 ml-[8px] whitespace-nowrap btn-small"
@click="selectedVariant = item"
v-for="item in variants"
:key="item.id"
>{{ item.title }}</Button
>
</div>
</div>
</div>
<div class="h-[55px]"></div>
<div class="w-full fixed bottom-0 z-20 left-0 right-0">
<div class="w-full bg-white flex justify-between p-4 border-t max-w-[768px] m-auto shadowset border-solid border-primary/10">
<Button type="primary" class="max-w-[768px] m-auto" size="large" block v-if="!cart.isInCart(storeProduct)" @click="isUserLoggedIn() ? addToCart() : (loginNotify = true)">
ثبت سفارش
</Button>
<router-link class="w-full max-w-[768px] m-auto" :to="{ name: 'cart' }" v-else>
<Button type="primary" size="large" block>مشاهده سبد خرید</Button>
</router-link>
</div>
</div>
<Drawer v-model:visible="showPopup" placement="bottom" :closable="false" height="60vh" :class="['henza-rounded-drawer rtl', 'bg-white', 'text-black']">
<div class="relative">
<div class="absolute top-0 z-10 w-full">
<Icon icon="vuesax-linear:arrow-right-1" @click="showPopup = false" class="cursor-pointer float-right ml-[30px] rounded-[100%] hover:bg-white/10" size="25" />
<h5>{{ title }}</h5>
</div>
</div>
<div class="h-[30px]"></div>
<div class="whitespace-pre-wrap text-justify leading-[2.5rem] max-h-[350px] overflow-y-auto">{{ body }}</div>
</Drawer>
</container>
<NotifyVant :timeOut="3000" v-model="notify" :color="'background-color: #22C55E'">
<div class="font-bold text-right mb-2">ثبت سفارش</div>
<div>محصول با موفقیت به سبد خرید افزوده شد</div>
</NotifyVant>
<DialogVant :timeOut="-1" v-model:show="loginNotify" :closeOnClickOverlay="true">
<template v-slot:img>
<img :src="loginDialogImage" alt="" class="opacity-80" />
</template>
<template v-slot:title>
<div class="p-8 text-justify">برای دسترسی به تمامی امکانات نرم افزار هنزاگلد، ابتدا گزینه ورود/ثبت نام را لمس کنید و در صورت نداشتن حساب کاربری، ثبت نام نمایید.</div>
</template>
<template v-slot:button>
<div class="text-center text-[#1C2D56]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
<div class="text-center text-danger" @click="loginNotify = false">بیخیال</div>
</template>
</DialogVant>
</Spinner>
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Button, Drawer } from 'ant-design-vue'
import Icon from '../icon/Icon.vue'
import Slider from '../slider/Slider.vue'
import Spinner from '../Spinner.vue'
import NotifyVant from '../NotifyVant.vue'
import DialogVant from '../DialogVant.vue'
import { useProduct } from '../../composables/useProduct'
import { useProductDetail } from '../../composables/useProductDetail'
import { useFavoritesStore } from '../../stores/favoritesStore'
import { useCartStore, type CartItem } from '../../stores/cartStore'
import { isUserLoggedIn } from '../../utils/checkAccess'
import loginDialogImage from '../../assets/img/empty/dialogEmpty1.svg'
const props = defineProps<{ id: string }>()
const emit = defineEmits<{ (e: 'closeDrawer'): void }>()
const router = useRouter()
// This component is used two ways, exactly as in legacy: embedded in a bottom
// Drawer by the product cards, and (legacy route `singleProduct`, /product/:id)
// as a standalone page for deep links. In the drawer case the parent listens
// for closeDrawer; on the standalone route there is no parent, so fall back to
// history.back() — legacy declared a goBack() for this but never wired it,
// leaving the close button dead on the routed page.
const close = () => {
emit('closeDrawer')
if (router.currentRoute.value.name === 'singleProduct') router.back()
}
const favorites = useFavoritesStore()
const cart = useCartStore()
const showPopup = ref(false)
const notify = ref(false)
const loginNotify = ref(false)
const promotion = ref<{ discount_percent?: number } | null>(null) // no promotions backend yet
const productDetail = useProductDetail(props.id)
const loading = productDetail.loading
const flag = productDetail.flag
// useProduct needs a real ProductSummary; give it an empty placeholder until
// the detail fetch resolves, matching legacy's own pattern of an initial
// empty-shaped `product` ref before data arrives.
const emptyProduct = { id: props.id, title: '', description: null, variants: [], images: [], category: null, brand: null, collections: [] }
const productAdapter = computed(() => useProduct(productDetail.productRef.value ?? emptyProduct))
const id = computed(() => productAdapter.value.id.value)
const title = computed(() => productAdapter.value.title.value)
const variants = computed(() => productAdapter.value.variants.value)
const selectedVariant = computed({
get: () => productAdapter.value.selectedVariant.value,
set: (v) => (productAdapter.value.selectedVariant.value = v),
})
const min_gram = computed(() => productAdapter.value.min_gram.value)
const wage = computed(() => productAdapter.value.wage.value)
const hasDiscountWage = computed(() => productAdapter.value.hasDiscountWage.value)
const body = computed(() => productDetail.productRef.value?.description ?? '')
const hasImages = computed(() => (productDetail.productRef.value?.images.length ?? 0) > 0)
const slides = computed(() => productDetail.productRef.value?.images.map((i) => i.url) ?? [])
// legacy: myStore.bankerPermission && hasDiscountWage && isUserLoggedIn() — no
// banker/login state exists yet, so this always evaluates false for now.
const showDiscountWage = computed(() => false && hasDiscountWage.value && isUserLoggedIn())
const storeProduct = computed<CartItem>(() => ({
vid: selectedVariant.value?.id ?? '',
note: null,
pid: productDetail.productRef.value?.id ?? props.id,
product: productDetail.productRef.value ?? (emptyProduct as any),
qty: 1,
}))
onBeforeMount(async () => {
await productDetail.load()
})
const addToCart = () => {
cart.addToCart(storeProduct.value)
notify.value = true
}
const toggleFav = (e: Event) => {
e.stopPropagation()
favorites.toggle(props.id)
}
// CRITICAL EXISTING ISSUE, ported as-is — see the file-level comment above.
history.pushState(null, location.href)
window.onpopstate = function () {
history.go(1)
}
</script>