chore(apps): remove unused code and packages (DEAD-01, DEAD-02, DEAD-03)
Owner approved the deletions. Found by walking imports from each app's main.ts; sw.ts stays (the PWA plugin's entry). - user-app: 8 unreachable components, loadingStore. - user-app-v2: 11 unreachable components, loadingStore. - both: the unused validators and price helpers, and the gsap and @ant-design/icons-vue packages. - the stray root user-app-v2/ folder (an old package.json and vite.config.ts that nothing referenced). Both apps type-check clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2d74f53d0a
commit
7bb5c2e00f
@@ -8,19 +8,17 @@
|
|||||||
"lint": "eslint \"src/**/*.{ts,vue}\""
|
"lint": "eslint \"src/**/*.{ts,vue}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons-vue": "^6.1.0",
|
|
||||||
"@gold-bank/shared-types": "workspace:*",
|
"@gold-bank/shared-types": "workspace:*",
|
||||||
|
"@lottiefiles/dotlottie-vue": "^0.4.15",
|
||||||
"@vueuse/core": "^9.13.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
"ant-design-vue": "^3.2.20",
|
"ant-design-vue": "^3.2.20",
|
||||||
"gsap": "^3.9.1",
|
|
||||||
"moment-jalaali": "^0.10.0",
|
"moment-jalaali": "^0.10.0",
|
||||||
"pinia": "^2.2.0",
|
"pinia": "^2.2.0",
|
||||||
"swiper": "^7.4.1",
|
"swiper": "^7.4.1",
|
||||||
"vant": "^4.0.3",
|
"vant": "^4.0.3",
|
||||||
"vue": "^3.5.0",
|
"vue": "^3.5.0",
|
||||||
"vue-router": "^4.4.0",
|
"vue-router": "^4.4.0",
|
||||||
"vue3-lazyload": "^0.2.5-beta",
|
"vue3-lazyload": "^0.2.5-beta"
|
||||||
"@lottiefiles/dotlottie-vue": "^0.4.15"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.1.0",
|
"@vitejs/plugin-vue": "^5.1.0",
|
||||||
@@ -30,8 +28,8 @@
|
|||||||
"tailwindcss": "3.0.18",
|
"tailwindcss": "3.0.18",
|
||||||
"typescript": "^5.5.0",
|
"typescript": "^5.5.0",
|
||||||
"vite": "^5.4.0",
|
"vite": "^5.4.0",
|
||||||
"vue-tsc": "^2.1.0",
|
|
||||||
"vite-plugin-pwa": "^0.20.5",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
|
"vue-tsc": "^2.1.0",
|
||||||
"workbox-precaching": "^7.1.0",
|
"workbox-precaching": "^7.1.0",
|
||||||
"workbox-window": "^7.1.0"
|
"workbox-window": "^7.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<!-- Ported from legacy henza_pwa/src/components/viewAll.vue.
|
|
||||||
Legacy left `to` undeclared and relied on Vue's attribute fallthrough to
|
|
||||||
land it on the root <router-link>; declared explicitly here only because
|
|
||||||
vue-tsc's template type-checking (which legacy's own build never ran)
|
|
||||||
requires router-link's `to` to be a known prop — same runtime value ends
|
|
||||||
up bound either way, no behavior change. -->
|
|
||||||
<template>
|
|
||||||
<router-link :to="to" class="text-primary w-[280px] rounded-[10px] p-[8px] bg-light flex flex-col justify-center items-center">
|
|
||||||
<Icon icon="vuesax-linear:arrow-left-1" class="cursor-pointer" size="25" />
|
|
||||||
مشاهده همه
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { RouteLocationRaw } from 'vue-router'
|
|
||||||
import Icon from './icon/Icon.vue'
|
|
||||||
|
|
||||||
defineProps<{ to: RouteLocationRaw }>()
|
|
||||||
</script>
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
<!--
|
|
||||||
The "حساب" block that sits BELOW the shop content on the home screen.
|
|
||||||
|
|
||||||
Two genuinely different states:
|
|
||||||
- signed out: a quiet invitation, no fabricated numbers.
|
|
||||||
- signed in: the customer's real gram position from GET /customers/:id
|
|
||||||
(goldDebtGrams / fiatDebtRial), labelled بدهکار or بستانکار the same way
|
|
||||||
DashboardView does, since the ledger stores debt as a signed figure.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<section class="acct">
|
|
||||||
<template v-if="customer">
|
|
||||||
<div class="acct__head">
|
|
||||||
<div>
|
|
||||||
<div class="acct__eyebrow">{{ positionLabel }}</div>
|
|
||||||
<div class="acct__figure">
|
|
||||||
{{ grams }}
|
|
||||||
<span class="acct__unit">گرم</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="acct__seal" :class="isDebtor ? 'is-debtor' : 'is-creditor'">
|
|
||||||
{{ isDebtor ? 'بدهکار' : 'بستانکار' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="acct__row">
|
|
||||||
<span class="acct__rowLabel">بدهی ریالی</span>
|
|
||||||
<span class="acct__rowValue">{{ price(fiat) }} <i>ریال</i></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="acct__actions">
|
|
||||||
<button type="button" class="acct__action" @click="emit('go', 'ordersList')">
|
|
||||||
<Icon icon="vuesax-linear:box" size="18" />
|
|
||||||
سفارشها
|
|
||||||
</button>
|
|
||||||
<button type="button" class="acct__action" @click="emit('go', 'transactionsList')">
|
|
||||||
<Icon icon="vuesax-linear:receipt-text" size="18" />
|
|
||||||
اسناد
|
|
||||||
</button>
|
|
||||||
<button type="button" class="acct__action" @click="emit('go', 'profileData')">
|
|
||||||
<Icon icon="vuesax-linear:user" size="18" />
|
|
||||||
پروفایل
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<div class="acct__invite">
|
|
||||||
<div class="acct__inviteIcon" aria-hidden="true">
|
|
||||||
<Icon icon="vuesax-linear:security-user" size="20" />
|
|
||||||
</div>
|
|
||||||
<div class="acct__inviteCopy">
|
|
||||||
<h3>حساب گرمی شما</h3>
|
|
||||||
<p>برای دیدن موجودی، سفارشها و اسناد خود وارد شوید.</p>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="acct__inviteCta" @click="emit('login')">ورود / ثبتنام</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import Icon from '../icon/Icon.vue'
|
|
||||||
import { price } from '../../utils/filters'
|
|
||||||
import type { CustomerDetail } from '../../api/client'
|
|
||||||
|
|
||||||
const props = defineProps<{ customer: CustomerDetail | null }>()
|
|
||||||
const emit = defineEmits<{ (e: 'go', route: string): void; (e: 'login'): void }>()
|
|
||||||
|
|
||||||
const isDebtor = computed(() => (props.customer ? parseFloat(props.customer.goldDebtGrams) > 0 : false))
|
|
||||||
const positionLabel = computed(() => (isDebtor.value ? 'مانده بدهی گرمی' : 'مانده بستانکاری گرمی'))
|
|
||||||
const grams = computed(() => (props.customer ? Math.abs(parseFloat(props.customer.goldDebtGrams)) : 0))
|
|
||||||
const fiat = computed(() => (props.customer ? props.customer.fiatDebtRial.split('.')[0] : '0'))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.acct {
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 18px;
|
|
||||||
background: @ink;
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 22px 44px -32px rgba(20, 22, 26, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__head {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__eyebrow {
|
|
||||||
font-size: 11px;
|
|
||||||
color: rgba(255, 255, 255, 0.55);
|
|
||||||
padding-bottom: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__figure {
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__unit {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: rgba(255, 255, 255, 0.55);
|
|
||||||
padding-inline-start: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__seal {
|
|
||||||
font-size: 10.5px;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 5px 11px;
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
.acct__seal.is-debtor {
|
|
||||||
color: #fff;
|
|
||||||
background: @wine;
|
|
||||||
}
|
|
||||||
.acct__seal.is-creditor {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.13);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 16px;
|
|
||||||
padding-top: 13px;
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
font-size: 12.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__rowLabel {
|
|
||||||
color: rgba(255, 255, 255, 0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__rowValue {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.acct__rowValue i {
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__actions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__action {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 11px 4px;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 13px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 11.5px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: rgba(255, 255, 255, 0.88);
|
|
||||||
background: rgba(255, 255, 255, 0.07);
|
|
||||||
transition: background 0.18s ease, transform 0.18s ease;
|
|
||||||
}
|
|
||||||
.acct__action:active {
|
|
||||||
transform: scale(0.97);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__invite {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 13px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__inviteIcon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 13px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
color: #fff;
|
|
||||||
background: @wine;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__inviteCopy {
|
|
||||||
flex: 1 1 150px;
|
|
||||||
}
|
|
||||||
.acct__inviteCopy h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.acct__inviteCopy p {
|
|
||||||
margin: 5px 0 0;
|
|
||||||
font-size: 11.5px;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: rgba(255, 255, 255, 0.58);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__inviteCta {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
margin-top: 4px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 13px;
|
|
||||||
background: #fff;
|
|
||||||
color: @ink;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
transition: transform 0.18s ease;
|
|
||||||
}
|
|
||||||
.acct__inviteCta:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
<!--
|
|
||||||
Horizontal category rail. Categories are real, derived from the product
|
|
||||||
catalogue by configStore (GET /catalog/products), so this renders nothing
|
|
||||||
rather than placeholder tiles when the catalogue is empty.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<div class="rail no-scrollbar">
|
|
||||||
<button
|
|
||||||
v-for="cat in categories"
|
|
||||||
:key="cat.id"
|
|
||||||
type="button"
|
|
||||||
class="rail__chip"
|
|
||||||
:class="{ 'is-active': cat.id === activeId }"
|
|
||||||
@click="emit('select', cat)"
|
|
||||||
>
|
|
||||||
<span class="rail__mark" aria-hidden="true">
|
|
||||||
<img v-if="cat.icon?.image" :src="cat.icon.image" alt="" />
|
|
||||||
</span>
|
|
||||||
<span class="rail__title">{{ cat.title }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { HomeCategory } from '../../stores/configStore'
|
|
||||||
|
|
||||||
defineProps<{ categories: HomeCategory[]; activeId?: string | null }>()
|
|
||||||
const emit = defineEmits<{ (e: 'select', cat: HomeCategory): void }>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.rail {
|
|
||||||
display: flex;
|
|
||||||
gap: 9px;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 2px 16px 2px;
|
|
||||||
margin: 0 -16px;
|
|
||||||
scroll-snap-type: x proximity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail__chip {
|
|
||||||
scroll-snap-align: start;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 14px 8px 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 12.5px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: @inkSoft;
|
|
||||||
background: @canvas;
|
|
||||||
border: 1px solid @hairline;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail__chip:active {
|
|
||||||
transform: scale(0.97);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail__chip.is-active {
|
|
||||||
background: @ink;
|
|
||||||
border-color: @ink;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail__mark {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
background: @hairlineSoft;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail__chip.is-active .rail__mark {
|
|
||||||
background: rgba(255, 255, 255, 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail__mark img {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
object-fit: contain;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail__title {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
<!--
|
|
||||||
Product card for the redesigned home screen.
|
|
||||||
|
|
||||||
Shows only fields that genuinely exist on ProductSummary today: title, the
|
|
||||||
cheapest variant's rash weight, its making-charge (اجرت) percent, and the
|
|
||||||
catalogue image. No price is printed on the card, because a finished piece's
|
|
||||||
rial price is a function of the live gold rate plus wage and is computed at
|
|
||||||
order time, not stored on the product — printing one here would be invented.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<article class="card" :class="{ 'card--wide': wide }" data-product-origin @click="emit('open', value)">
|
|
||||||
<div class="card__art">
|
|
||||||
<img v-lazy="image" :alt="title" />
|
|
||||||
<button type="button" class="card__fav" :class="{ 'is-on': favorites.isFavorite(id) }" @click.stop="favorites.toggle(id)">
|
|
||||||
<Icon :icon="favorites.isFavorite(id) ? 'vuesax-linear:heart-fill' : 'vuesax-linear:heart'" size="17" />
|
|
||||||
</button>
|
|
||||||
<span v-if="badge" class="card__badge">{{ badge }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card__body">
|
|
||||||
<h3 class="card__title">{{ title }}</h3>
|
|
||||||
<div class="card__meta">
|
|
||||||
<span class="card__weight">{{ min_gram }} گرم</span>
|
|
||||||
<span v-if="wage != null" class="card__wage">اجرت {{ wageLabel }}٪</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import Icon from '../icon/Icon.vue'
|
|
||||||
import { useProduct } from '../../composables/useProduct'
|
|
||||||
import { useFavoritesStore } from '../../stores/favoritesStore'
|
|
||||||
import type { ProductSummary } from '../../api/client'
|
|
||||||
|
|
||||||
const props = defineProps<{ value: ProductSummary; wide?: boolean; badge?: string }>()
|
|
||||||
const emit = defineEmits<{ (e: 'open', value: ProductSummary): void }>()
|
|
||||||
|
|
||||||
const favorites = useFavoritesStore()
|
|
||||||
const { image, id, title, min_gram, wage } = useProduct(props.value)
|
|
||||||
|
|
||||||
const wageLabel = computed(() => {
|
|
||||||
const w = wage.value
|
|
||||||
if (w == null) return ''
|
|
||||||
return w % 1 === 0 ? String(w) : w.toFixed(2)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.card {
|
|
||||||
cursor: pointer;
|
|
||||||
background: @canvas;
|
|
||||||
border: 1px solid @hairline;
|
|
||||||
border-radius: 18px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:active {
|
|
||||||
transform: scale(0.985);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__art {
|
|
||||||
position: relative;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: @hairlineSoft;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card--wide .card__art {
|
|
||||||
aspect-ratio: 4 / 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__art img {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 200px;
|
|
||||||
height: 100%;
|
|
||||||
max-height: 200px;
|
|
||||||
object-fit: contain;
|
|
||||||
object-position: center;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__fav {
|
|
||||||
position: absolute;
|
|
||||||
top: 9px;
|
|
||||||
left: 9px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: @inkMuted;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
transition: color 0.18s ease, transform 0.18s ease;
|
|
||||||
}
|
|
||||||
.card__fav.is-on {
|
|
||||||
color: @wine;
|
|
||||||
}
|
|
||||||
.card__fav:active {
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 9px;
|
|
||||||
right: 9px;
|
|
||||||
padding: 4px 9px;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
background: @wine;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__body {
|
|
||||||
padding: 11px 12px 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: @ink;
|
|
||||||
line-height: 1.6;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
padding-top: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__weight {
|
|
||||||
font-size: 11.5px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: @inkSoft;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__wage {
|
|
||||||
font-size: 10.5px;
|
|
||||||
color: @inkFaint;
|
|
||||||
padding-inline-start: 7px;
|
|
||||||
border-inline-start: 1px solid @hairline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
<!--
|
|
||||||
Editorial hero for the home screen. Replaces the legacy swiper banner with a
|
|
||||||
self-contained, dependency-free slider: no swiper CSS to fight, no autoplay
|
|
||||||
jitter, and it keeps working when a slide has no artwork.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<section class="showcase" v-if="slides.length">
|
|
||||||
<div class="showcase__frame">
|
|
||||||
<transition-group name="showcase-fade" tag="div" class="showcase__stage">
|
|
||||||
<article v-for="(slide, i) in slides" :key="slide.key" v-show="i === index" class="showcase__slide">
|
|
||||||
<img v-if="slide.image" :src="slide.image" alt="" class="showcase__art" />
|
|
||||||
<div class="showcase__scrim" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
<div class="showcase__copy">
|
|
||||||
<span class="showcase__eyebrow">{{ slide.eyebrow }}</span>
|
|
||||||
<h2 class="showcase__title">{{ slide.title }}</h2>
|
|
||||||
<p class="showcase__sub">{{ slide.subtitle }}</p>
|
|
||||||
<button type="button" class="showcase__cta" @click="emit('select', slide)">
|
|
||||||
{{ slide.cta }}
|
|
||||||
<Icon icon="vuesax-linear:arrow-left-2" size="17" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</transition-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="showcase__dots" v-if="slides.length > 1">
|
|
||||||
<button
|
|
||||||
v-for="(slide, i) in slides"
|
|
||||||
:key="slide.key"
|
|
||||||
type="button"
|
|
||||||
class="showcase__dot"
|
|
||||||
:class="{ 'is-active': i === index }"
|
|
||||||
:aria-label="slide.title"
|
|
||||||
@click="go(i)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
||||||
import Icon from '../icon/Icon.vue'
|
|
||||||
|
|
||||||
export interface ShowcaseSlide {
|
|
||||||
key: string
|
|
||||||
eyebrow: string
|
|
||||||
title: string
|
|
||||||
subtitle: string
|
|
||||||
cta: string
|
|
||||||
image?: string
|
|
||||||
categoryId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{ slides: ShowcaseSlide[] }>()
|
|
||||||
const emit = defineEmits<{ (e: 'select', slide: ShowcaseSlide): void }>()
|
|
||||||
|
|
||||||
const index = ref(0)
|
|
||||||
let timer: ReturnType<typeof setInterval> | undefined
|
|
||||||
|
|
||||||
function go(i: number) {
|
|
||||||
index.value = i
|
|
||||||
restart()
|
|
||||||
}
|
|
||||||
function restart() {
|
|
||||||
if (timer) clearInterval(timer)
|
|
||||||
if (props.slides.length > 1) {
|
|
||||||
timer = setInterval(() => {
|
|
||||||
index.value = (index.value + 1) % props.slides.length
|
|
||||||
}, 6000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(restart)
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (timer) clearInterval(timer)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.showcase__frame {
|
|
||||||
position: relative;
|
|
||||||
border-radius: 22px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: @ink;
|
|
||||||
box-shadow: 0 26px 50px -34px rgba(20, 22, 26, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase__stage {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
aspect-ratio: 320 / 208;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase__slide {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase__art {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
opacity: 0.5;
|
|
||||||
transform: scale(1.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase__scrim {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background:
|
|
||||||
linear-gradient(255deg, rgba(122, 27, 51, 0.55) 0%, rgba(122, 27, 51, 0) 52%),
|
|
||||||
linear-gradient(to left, rgba(20, 22, 26, 0.2) 0%, rgba(20, 22, 26, 0.92) 72%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase__copy {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
padding: 22px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase__eyebrow {
|
|
||||||
font-size: 10.5px;
|
|
||||||
letter-spacing: 1.6px;
|
|
||||||
color: rgba(255, 255, 255, 0.62);
|
|
||||||
padding-bottom: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase__title {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1.35;
|
|
||||||
color: #fff;
|
|
||||||
margin: 0 0 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase__sub {
|
|
||||||
font-size: 12.5px;
|
|
||||||
line-height: 1.85;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
margin: 0 0 16px;
|
|
||||||
max-width: 30ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase__cta {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 9px 16px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #fff;
|
|
||||||
color: @ink;
|
|
||||||
font-size: 12.5px;
|
|
||||||
font-weight: 700;
|
|
||||||
font-family: inherit;
|
|
||||||
transition: transform 0.18s ease, background 0.18s ease;
|
|
||||||
}
|
|
||||||
.showcase__cta:active {
|
|
||||||
transform: scale(0.97);
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase__dots {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase__dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: @hairline;
|
|
||||||
transition: all 0.25s ease;
|
|
||||||
}
|
|
||||||
.showcase__dot.is-active {
|
|
||||||
width: 20px;
|
|
||||||
background: @wine;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showcase-fade-enter-active,
|
|
||||||
.showcase-fade-leave-active {
|
|
||||||
transition: opacity 0.55s ease;
|
|
||||||
}
|
|
||||||
.showcase-fade-enter-from,
|
|
||||||
.showcase-fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<!--
|
|
||||||
Ported from legacy henza_pwa/src/components/product/category.vue.
|
|
||||||
Not yet wired to real data: the new API has no categories-with-icon endpoint
|
|
||||||
(Prisma has a Category model, but no controller/route exposes it, and there
|
|
||||||
are no per-category icon images). HomeView passes an empty list for now, so
|
|
||||||
this renders nothing rather than fabricated placeholder tiles — see the
|
|
||||||
"existing issue found" note in the report about the legacy `id === 45`
|
|
||||||
special-case, preserved here for whenever real category data exists.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<router-link :to="{ name: 'search' }" @click="setCats" class="cursor-pointer h-full block">
|
|
||||||
<div
|
|
||||||
class="h-full w-full text-center flex flex-col items-center justify-between rounded-[4px]"
|
|
||||||
:class="value.id === '45' ? 'bg-primary' : 'border border-1 border-secondary'"
|
|
||||||
>
|
|
||||||
<div class="flex justify-center mt-4 shrink-0">
|
|
||||||
<img v-lazy="value.icon?.image" alt="category" class="w-[32px] h-[32px] text-center" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="mb-4 mt-4 py-1 px-1 font-semibold flex-1 flex items-center justify-center text-center min-h-[40px]"
|
|
||||||
:class="value.id === '45' ? 'text-white' : 'text-primary'"
|
|
||||||
>
|
|
||||||
{{ value.title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useFiltersStore } from '../../stores/filtersStore'
|
|
||||||
|
|
||||||
interface CategoryValue {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
icon?: { image: string }
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ value: CategoryValue }>(), {})
|
|
||||||
const filters = useFiltersStore()
|
|
||||||
|
|
||||||
const setCats = () => {
|
|
||||||
filters.query.cats = [props.value.id]
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
<!--
|
|
||||||
Ported from legacy henza_pwa/src/components/slider/homeSlider.vue.
|
|
||||||
`$prefix` (legacy global property, always the compile-time-constant 'montzar'
|
|
||||||
from themeConfig) is inlined directly here rather than re-registered as a
|
|
||||||
global — same resolved value, no behavior change.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Swiper
|
|
||||||
:grabCursor="true"
|
|
||||||
:centeredSlides="true"
|
|
||||||
:space-between="5"
|
|
||||||
:loop="true"
|
|
||||||
:slidesPerView="1.3"
|
|
||||||
class="w-full home-slider"
|
|
||||||
:autoplay="data.autoplay"
|
|
||||||
:pagination="{ clickable: true }"
|
|
||||||
>
|
|
||||||
<SwiperSlide class="bg-center bg-cover h-auto" v-for="(item, index) in slides" :key="index">
|
|
||||||
<img @click="clickHandler(item)" v-lazy="item.src" width="100%" class="block w-full rounded-[10px] object-cover" :style="`height:${height}`" />
|
|
||||||
</SwiperSlide>
|
|
||||||
</Swiper>
|
|
||||||
<Drawer
|
|
||||||
v-model:visible="data.show"
|
|
||||||
placement="bottom"
|
|
||||||
:closable="false"
|
|
||||||
@close="setAutoPlay"
|
|
||||||
height="60%"
|
|
||||||
class="montzar-rounded-drawer rtl text-white bg-primaryLighten whitespace-pre-wrap text[13px]"
|
|
||||||
>
|
|
||||||
{{ data.slideContent.text }}
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
|
||||||
import 'swiper/css'
|
|
||||||
import 'swiper/css/navigation'
|
|
||||||
import 'swiper/css/pagination'
|
|
||||||
import 'swiper/css/scrollbar'
|
|
||||||
import SwiperCore, { Autoplay, Navigation, Pagination } from 'swiper'
|
|
||||||
SwiperCore.use([Pagination, Navigation, Autoplay])
|
|
||||||
import { reactive } from 'vue'
|
|
||||||
import { Drawer } from 'ant-design-vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useFiltersStore } from '../../stores/filtersStore'
|
|
||||||
|
|
||||||
interface Slide {
|
|
||||||
text: string | undefined
|
|
||||||
type: string | undefined
|
|
||||||
src: string | undefined
|
|
||||||
link: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
show: boolean
|
|
||||||
slideContent: Slide
|
|
||||||
autoplay: { delay: number; disableOnInteraction: boolean } | boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<{ slides: Slide[]; height: string }>(), {})
|
|
||||||
|
|
||||||
const data: Data = reactive({
|
|
||||||
show: false,
|
|
||||||
slideContent: { text: undefined, type: undefined, src: undefined, link: undefined },
|
|
||||||
autoplay: { delay: 4000, disableOnInteraction: false },
|
|
||||||
})
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const filters = useFiltersStore()
|
|
||||||
|
|
||||||
const clickHandler = async (item: Slide) => {
|
|
||||||
if (item) {
|
|
||||||
data.slideContent = item
|
|
||||||
switch (item.type) {
|
|
||||||
case 'popup':
|
|
||||||
if (item && !(!item.text || item.text.length === 0)) data.show = true
|
|
||||||
break
|
|
||||||
case 'link':
|
|
||||||
if (item && !(!item.link || item.link.length === 0)) window.open(item.link)?.focus()
|
|
||||||
break
|
|
||||||
case 'discount':
|
|
||||||
if (!(!item.link || item.link.length === 0)) {
|
|
||||||
// EXISTING ISSUE, ported as-is: legacy sets `query.value.discount`,
|
|
||||||
// but the actual filter field is `has_discount` — this assignment
|
|
||||||
// never affected search filtering in legacy either. Not "fixed"
|
|
||||||
// here; flagged separately.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
;(filters.query as any).discount = true
|
|
||||||
await router.push({ name: 'search' })
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
// code block
|
|
||||||
}
|
|
||||||
data.autoplay = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const setAutoPlay = async () => {
|
|
||||||
data.autoplay = { delay: 4000, disableOnInteraction: false }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.home-slider {
|
|
||||||
&.swiper {
|
|
||||||
padding: 24px 0 24px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullet {
|
|
||||||
transition: all 0.2s;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullet-active,
|
|
||||||
.swiper-pagination-bullet-active-main {
|
|
||||||
width: 24px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-horizontal > .swiper-pagination-bullets,
|
|
||||||
.swiper-pagination-bullets.swiper-pagination-horizontal,
|
|
||||||
.swiper-pagination-custom,
|
|
||||||
.swiper-pagination-fraction {
|
|
||||||
bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullets-dynamic * {
|
|
||||||
transform: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullet-active {
|
|
||||||
background: @primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-slide {
|
|
||||||
transition: all 0.6s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-slide:not(.swiper-slide-active) {
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
<!--
|
|
||||||
Ported from legacy henza_pwa/src/components/slider/slider.vue.
|
|
||||||
EXISTING ISSUE, ported as-is (not fixed): `showPopup` takes (src, index) but
|
|
||||||
the template calls it with a single argument (`showPopup(item)`), and the
|
|
||||||
v-for doesn't even destructure an index. `index` is therefore always
|
|
||||||
undefined inside showPopup, so its "rotate the slides array to start at the
|
|
||||||
tapped image" logic never runs — tapping any thumbnail always opens the
|
|
||||||
full-screen zoom viewer starting from the first image, never the tapped one.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Swiper :grabCursor="true" :centeredSlides="true" :loop="false" :slidesPerView="1" class="w-full slider" :pagination="{ clickable: true }">
|
|
||||||
<SwiperSlide class="bg-center bg-cover h-auto" v-for="item in slides" :key="item">
|
|
||||||
<img @click="showPopup(item)" v-lazy="item" class="block w-auto object-cover z-0 pb-1" :style="`height:${height}`" />
|
|
||||||
</SwiperSlide>
|
|
||||||
<transition name="slideUp" mode="out-in">
|
|
||||||
<svg alt="منتزر" class="absolute bottom-0 z-[2] m-auto pointer-events-none w-full h-[90px]" v-if="flagSvg">
|
|
||||||
<path fill="#FFFFFF" class="w-full h-[90px]" :d="whiteShape" />
|
|
||||||
<path fill="#3E5279" class="w-full h-[90px]" :d="curvedShape" />
|
|
||||||
</svg>
|
|
||||||
</transition>
|
|
||||||
</Swiper>
|
|
||||||
|
|
||||||
<Drawer v-model:visible="show" placement="bottom" :closable="false" :bodyStyle="{ padding: 0 }" class="rtl whitespace-pre-wrap bg-white" height="100vh">
|
|
||||||
<div @click="show = false" class="fixed top-[30px] right-[20px] z-[5] pb-2 pl-5">
|
|
||||||
<Icon icon="vuesax-linear:arrow-right-1" @click="show = false" class="cursor-pointer text-white bg-secondary rounded-[5px] p-[2.5px]" size="28" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Swiper :grabCursor="true" :zoom="{ maxRatio: 1.2 }" :modules="[Zoom]" :centeredSlides="true" :loop="false" :slidesPerView="1" class="w-full slider" :key="show">
|
|
||||||
<SwiperSlide class="bg-center bg-cover h-auto" v-for="item in slides" :key="item">
|
|
||||||
<div class="swiper-zoom-container">
|
|
||||||
<img v-lazy="item" class="block object-contain w-[100vw] h-screen" />
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
</Swiper>
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onBeforeMount, ref } from 'vue'
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
|
||||||
import 'swiper/css'
|
|
||||||
import 'swiper/css/navigation'
|
|
||||||
import 'swiper/css/pagination'
|
|
||||||
import 'swiper/css/scrollbar'
|
|
||||||
import 'swiper/css/zoom'
|
|
||||||
import SwiperCore, { Navigation, Pagination, Zoom } from 'swiper'
|
|
||||||
import { Drawer } from 'ant-design-vue'
|
|
||||||
import Icon from '../icon/Icon.vue'
|
|
||||||
import { useWindowDataStore } from '../../stores/windowDataStore'
|
|
||||||
|
|
||||||
SwiperCore.use([Pagination, Navigation])
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ slides: string[]; height: string; width?: string }>(), {})
|
|
||||||
|
|
||||||
const show = ref(false)
|
|
||||||
const slides = ref<string[]>([...props.slides])
|
|
||||||
|
|
||||||
const showPopup = (src: string, index?: number) => {
|
|
||||||
slides.value = [...props.slides]
|
|
||||||
for (let i = 0; i < (index ?? 0); i++) {
|
|
||||||
const helper = slides.value.shift()
|
|
||||||
if (helper) slides.value.push(helper)
|
|
||||||
}
|
|
||||||
show.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const flagSvg = ref(false)
|
|
||||||
onBeforeMount(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
flagSvg.value = true
|
|
||||||
}, 100)
|
|
||||||
})
|
|
||||||
|
|
||||||
const windowData = useWindowDataStore()
|
|
||||||
const shapeHeight = 90
|
|
||||||
const wcp1x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 21.8878249)
|
|
||||||
const wcp2x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 2.37406336)
|
|
||||||
const crcr1cp1x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 1.56947374)
|
|
||||||
const crcr1cp2x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 5.47609844)
|
|
||||||
const crcr2cp1x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 15.8024691)
|
|
||||||
const crcr2cp2x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 2.2606383)
|
|
||||||
const whiteShape = computed(() => `M0,${shapeHeight}H${Math.min(windowData.width, 768)}V0C${wcp1x.value},19.17,${wcp2x.value},119.08,0,49Z`)
|
|
||||||
const curvedShape = computed(
|
|
||||||
() => `M0,39.77C${crcr1cp1x.value},91.74,${crcr1cp2x.value},61.5,${Math.min(windowData.width, 768)},0,C${crcr2cp1x.value},29.34,${crcr2cp2x.value},112.54,0,57.1Z`,
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.slider {
|
|
||||||
.swiper-pagination-bullet {
|
|
||||||
transition: all 0.2s;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullet-active,
|
|
||||||
.swiper-pagination-bullet-active-main {
|
|
||||||
width: 24px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-horizontal > .swiper-pagination-bullets,
|
|
||||||
.swiper-pagination-bullets.swiper-pagination-horizontal,
|
|
||||||
.swiper-pagination-custom,
|
|
||||||
.swiper-pagination-fraction {
|
|
||||||
bottom: 40px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullets-dynamic * {
|
|
||||||
transform: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullet-active {
|
|
||||||
background: @primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-slide img {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.slideUp-enter-active,
|
|
||||||
.slideUp-leave-active {
|
|
||||||
opacity: 1;
|
|
||||||
transition: all @transition-duration ease @transition-delay;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slideUp-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slideUp-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!-- Ported from legacy henza_pwa/src/components/content.vue -->
|
|
||||||
<template>
|
|
||||||
<main :style="{ marginTop: transition.transitionEnded ? `${transition.headerHeight}px` : 'unset' }">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useTransitionStore } from '../../stores/transitionStore'
|
|
||||||
|
|
||||||
const transition = useTransitionStore()
|
|
||||||
</script>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<!-- Ported from legacy henza_pwa/src/components/ui/ScrollFetch.vue — the plain
|
|
||||||
infinite-scroll container (default ant Empty, no custom illustration). -->
|
|
||||||
<template>
|
|
||||||
<main
|
|
||||||
class="overflow-auto no-scrollbar w-[100vw] h-screen max-w-screen-md m-auto"
|
|
||||||
:style="{ paddingTop: padding || transition.headerHeight + 'px' }"
|
|
||||||
ref="scrollArea"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
<Spinner cls="my-[10px]" :spinning="loading" :indicator="true" tip="درحال دریافت اطلاعات" />
|
|
||||||
<EmptyState v-if="flag" />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useInfiniteScroll } from '@vueuse/core'
|
|
||||||
import Spinner from '../Spinner.vue'
|
|
||||||
import EmptyState from '../EmptyState.vue'
|
|
||||||
import { useTransitionStore } from '../../stores/transitionStore'
|
|
||||||
|
|
||||||
withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
loading: boolean
|
|
||||||
flag: boolean
|
|
||||||
padding?: string
|
|
||||||
}>(),
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
const emit = defineEmits<{ (e: 'fetch'): void }>()
|
|
||||||
const transition = useTransitionStore()
|
|
||||||
const scrollArea = ref(null)
|
|
||||||
|
|
||||||
useInfiniteScroll(
|
|
||||||
scrollArea,
|
|
||||||
() => {
|
|
||||||
emit('fetch')
|
|
||||||
},
|
|
||||||
{ distance: 50 },
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<!--
|
|
||||||
Ported from legacy henza_pwa/src/components/ui/ScrollFetchAfterSearch.vue.
|
|
||||||
Legacy imported emp_after_search.svg / emp_before_search.svg and an `isSearch`
|
|
||||||
prop to pick between them, but the live template used neither — it rendered
|
|
||||||
`ordersEmpty.svg` unconditionally. Ported as-is (unused imports dropped,
|
|
||||||
`isSearch` kept since it is part of the component's declared prop contract).
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<main
|
|
||||||
class="overflow-auto no-scrollbar w-[100vw] h-screen max-w-screen-md m-auto"
|
|
||||||
:style="{ paddingTop: padding || transition.headerHeight + 'px' }"
|
|
||||||
ref="scrollArea"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
<Spinner cls="my-[10px]" :spinning="loading" :indicator="true" tip="درحال دریافت اطلاعات" />
|
|
||||||
<EmptyState v-if="flag && !loading" :image="ordersEmpty" />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useInfiniteScroll } from '@vueuse/core'
|
|
||||||
import Spinner from '../Spinner.vue'
|
|
||||||
import EmptyState from '../EmptyState.vue'
|
|
||||||
import ordersEmpty from '../../assets/img/empty/ordersEmpty.svg'
|
|
||||||
import { useTransitionStore } from '../../stores/transitionStore'
|
|
||||||
|
|
||||||
withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
loading: boolean
|
|
||||||
flag: boolean
|
|
||||||
padding?: string
|
|
||||||
isSearch?: boolean
|
|
||||||
}>(),
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
const emit = defineEmits<{ (e: 'fetch'): void }>()
|
|
||||||
const transition = useTransitionStore()
|
|
||||||
const scrollArea = ref(null)
|
|
||||||
|
|
||||||
useInfiniteScroll(
|
|
||||||
scrollArea,
|
|
||||||
() => {
|
|
||||||
emit('fetch')
|
|
||||||
},
|
|
||||||
{ distance: 50 },
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
// Ported from legacy henza_pwa/src/store/loadingStore.ts — a single global
|
|
||||||
// loading flag, converted from the hand-rolled ref-class to Pinia.
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
|
|
||||||
export const useLoadingStore = defineStore('loading', {
|
|
||||||
state: () => ({
|
|
||||||
loading: false,
|
|
||||||
}),
|
|
||||||
actions: {
|
|
||||||
setLoading(loading: boolean) {
|
|
||||||
this.loading = loading
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -11,20 +11,6 @@ export function price(val: number | string | undefined | null): string {
|
|||||||
return val as any
|
return val as any
|
||||||
}
|
}
|
||||||
|
|
||||||
export function priceUnit(val: number | string | undefined | null): string {
|
|
||||||
if (['string', 'number'].includes(typeof val)) {
|
|
||||||
return `${val!.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')} تومان `
|
|
||||||
}
|
|
||||||
return val as any
|
|
||||||
}
|
|
||||||
|
|
||||||
export function priceUnitRialLess(val: number | string | undefined | null): string {
|
|
||||||
if (['string', 'number'].includes(typeof val)) {
|
|
||||||
return `${val!.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')} `
|
|
||||||
}
|
|
||||||
return val as any
|
|
||||||
}
|
|
||||||
|
|
||||||
export function jdate(val: string | undefined | null): string {
|
export function jdate(val: string | undefined | null): string {
|
||||||
if (val) {
|
if (val) {
|
||||||
// legacy parsed a fixed 'YYYY-MM-DD HH:mm:ss' Laravel timestamp format;
|
// legacy parsed a fixed 'YYYY-MM-DD HH:mm:ss' Laravel timestamp format;
|
||||||
|
|||||||
@@ -1,30 +1,9 @@
|
|||||||
// Ported from legacy henza_pwa/src/Mixins/validator/index.ts — same rules,
|
// Ported from legacy henza_pwa/src/Mixins/validator/index.ts — same rules,
|
||||||
// same messages, verbatim.
|
// same messages, verbatim. Only the rules the app uses are kept (DEAD-01/02).
|
||||||
export const message = (field: string) => ` مقدار ${field} نا معتبر است `
|
export const message = (field: string) => ` مقدار ${field} نا معتبر است `
|
||||||
|
|
||||||
export const mobile = (field: string) => ({
|
export const mobile = (field: string) => ({
|
||||||
pattern: /^(\+98|0098|98|0)?9\d{9}$/g,
|
pattern: /^(\+98|0098|98|0)?9\d{9}$/g,
|
||||||
message: message(field),
|
message: message(field),
|
||||||
})
|
})
|
||||||
export const phone = (field: string) => ({
|
|
||||||
pattern: /^((\+98|0098|98|0)\d1)?\d{8}$/g,
|
|
||||||
message: message(field),
|
|
||||||
})
|
|
||||||
export const fa = (field: string) => ({
|
|
||||||
pattern: /^([-ۿﮊپچگ ])+$/,
|
|
||||||
message: ` مقدار ${field} باید فارسی باشد `,
|
|
||||||
})
|
|
||||||
export const mail = (field: string) => ({ type: 'email', message: message(field) })
|
|
||||||
export const url = (field: string) => ({ type: 'url', message: message(field) })
|
|
||||||
export const username = (field: string) => ({
|
|
||||||
pattern: /^(?!.*\.\.)(?!.*\.$)[^\W][\w.]{0,29}$/gim,
|
|
||||||
message: ` مقدار ${field} باید شامل حروف و اعداد انگلیسی و _ باشد `,
|
|
||||||
})
|
|
||||||
export const required = (field: string) => ({ required: true, message: ` لطفا ${field} را وارد کنید ` })
|
export const required = (field: string) => ({ required: true, message: ` لطفا ${field} را وارد کنید ` })
|
||||||
export const sheba = (field: string) => ({ pattern: /^IR\d{24}$/gim, message: message(field) })
|
|
||||||
export const card = (field: string) => ({ pattern: /^\d{16}$/gim, message: message(field) })
|
|
||||||
export const zip = (field: string) => ({ pattern: /^\d{10}$/gim, message: message(field) })
|
|
||||||
export const num = (field: string) => ({ pattern: /^\d*$/gim, message: message(field) })
|
|
||||||
export const len = (field: string, len: number) => ({ len, message: ` مقدار ${field} باید ${len} کارکتر باشد ` })
|
|
||||||
export const max = (field: string, max: number) => ({ max, message: ` مقدار ${field} باید کمتر از ${max} کارکتر باشد ` })
|
|
||||||
export const min = (field: string, min: number) => ({ min, message: ` مقدار ${field} باید بیشتر از ${min} کارکتر باشد ` })
|
|
||||||
|
|||||||
@@ -8,19 +8,17 @@
|
|||||||
"lint": "eslint \"src/**/*.{ts,vue}\""
|
"lint": "eslint \"src/**/*.{ts,vue}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons-vue": "^6.1.0",
|
|
||||||
"@gold-bank/shared-types": "workspace:*",
|
"@gold-bank/shared-types": "workspace:*",
|
||||||
|
"@lottiefiles/dotlottie-vue": "^0.4.15",
|
||||||
"@vueuse/core": "^9.13.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
"ant-design-vue": "^3.2.20",
|
"ant-design-vue": "^3.2.20",
|
||||||
"gsap": "^3.9.1",
|
|
||||||
"moment-jalaali": "^0.10.0",
|
"moment-jalaali": "^0.10.0",
|
||||||
"pinia": "^2.2.0",
|
"pinia": "^2.2.0",
|
||||||
"swiper": "^7.4.1",
|
"swiper": "^7.4.1",
|
||||||
"vant": "^4.0.3",
|
"vant": "^4.0.3",
|
||||||
"vue": "^3.5.0",
|
"vue": "^3.5.0",
|
||||||
"vue-router": "^4.4.0",
|
"vue-router": "^4.4.0",
|
||||||
"vue3-lazyload": "^0.2.5-beta",
|
"vue3-lazyload": "^0.2.5-beta"
|
||||||
"@lottiefiles/dotlottie-vue": "^0.4.15"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.1.0",
|
"@vitejs/plugin-vue": "^5.1.0",
|
||||||
@@ -30,8 +28,8 @@
|
|||||||
"tailwindcss": "3.0.18",
|
"tailwindcss": "3.0.18",
|
||||||
"typescript": "^5.5.0",
|
"typescript": "^5.5.0",
|
||||||
"vite": "^5.4.0",
|
"vite": "^5.4.0",
|
||||||
"vue-tsc": "^2.1.0",
|
|
||||||
"vite-plugin-pwa": "^0.20.5",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
|
"vue-tsc": "^2.1.0",
|
||||||
"workbox-precaching": "^7.1.0",
|
"workbox-precaching": "^7.1.0",
|
||||||
"workbox-window": "^7.1.0"
|
"workbox-window": "^7.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<!-- Ported from legacy henza_pwa/src/components/viewAll.vue.
|
|
||||||
Legacy left `to` undeclared and relied on Vue's attribute fallthrough to
|
|
||||||
land it on the root <router-link>; declared explicitly here only because
|
|
||||||
vue-tsc's template type-checking (which legacy's own build never ran)
|
|
||||||
requires router-link's `to` to be a known prop — same runtime value ends
|
|
||||||
up bound either way, no behavior change. -->
|
|
||||||
<template>
|
|
||||||
<router-link :to="to" class="text-primary w-[280px] rounded-[10px] p-[8px] bg-light flex flex-col justify-center items-center">
|
|
||||||
<Icon icon="vuesax-linear:arrow-left-1" class="cursor-pointer" size="25" />
|
|
||||||
مشاهده همه
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { RouteLocationRaw } from 'vue-router'
|
|
||||||
import Icon from './icon/Icon.vue'
|
|
||||||
|
|
||||||
defineProps<{ to: RouteLocationRaw }>()
|
|
||||||
</script>
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
<!--
|
|
||||||
The "حساب" block that sits BELOW the shop content on the home screen.
|
|
||||||
|
|
||||||
Two genuinely different states:
|
|
||||||
- signed out: a quiet invitation, no fabricated numbers.
|
|
||||||
- signed in: the customer's real gram position from GET /customers/:id
|
|
||||||
(goldDebtGrams / fiatDebtRial), labelled بدهکار or بستانکار the same way
|
|
||||||
DashboardView does, since the ledger stores debt as a signed figure.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<section class="acct">
|
|
||||||
<template v-if="customer">
|
|
||||||
<div class="acct__head">
|
|
||||||
<div>
|
|
||||||
<div class="acct__eyebrow">{{ positionLabel }}</div>
|
|
||||||
<div class="acct__figure">
|
|
||||||
{{ grams }}
|
|
||||||
<span class="acct__unit">گرم</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="acct__seal" :class="isDebtor ? 'is-debtor' : 'is-creditor'">
|
|
||||||
{{ isDebtor ? 'بدهکار' : 'بستانکار' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="acct__row">
|
|
||||||
<span class="acct__rowLabel">بدهی ریالی</span>
|
|
||||||
<span class="acct__rowValue">{{ price(fiat) }} <i>ریال</i></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="acct__actions">
|
|
||||||
<button type="button" class="acct__action" @click="emit('go', 'ordersList')">
|
|
||||||
<Icon icon="vuesax-linear:box" size="18" />
|
|
||||||
سفارشها
|
|
||||||
</button>
|
|
||||||
<button type="button" class="acct__action" @click="emit('go', 'transactionsList')">
|
|
||||||
<Icon icon="vuesax-linear:receipt-text" size="18" />
|
|
||||||
اسناد
|
|
||||||
</button>
|
|
||||||
<button type="button" class="acct__action" @click="emit('go', 'profileData')">
|
|
||||||
<Icon icon="vuesax-linear:user" size="18" />
|
|
||||||
پروفایل
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<div class="acct__invite">
|
|
||||||
<div class="acct__inviteIcon" aria-hidden="true">
|
|
||||||
<Icon icon="vuesax-linear:security-user" size="20" />
|
|
||||||
</div>
|
|
||||||
<div class="acct__inviteCopy">
|
|
||||||
<h3>حساب گرمی شما</h3>
|
|
||||||
<p>برای دیدن موجودی، سفارشها و اسناد خود وارد شوید.</p>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="acct__inviteCta" @click="emit('login')">ورود / ثبتنام</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import Icon from '../icon/Icon.vue'
|
|
||||||
import { price } from '../../utils/filters'
|
|
||||||
import type { CustomerDetail } from '../../api/client'
|
|
||||||
|
|
||||||
const props = defineProps<{ customer: CustomerDetail | null }>()
|
|
||||||
const emit = defineEmits<{ (e: 'go', route: string): void; (e: 'login'): void }>()
|
|
||||||
|
|
||||||
const isDebtor = computed(() => (props.customer ? parseFloat(props.customer.goldDebtGrams) > 0 : false))
|
|
||||||
const positionLabel = computed(() => (isDebtor.value ? 'مانده بدهی گرمی' : 'مانده بستانکاری گرمی'))
|
|
||||||
const grams = computed(() => (props.customer ? Math.abs(parseFloat(props.customer.goldDebtGrams)) : 0))
|
|
||||||
const fiat = computed(() => (props.customer ? props.customer.fiatDebtRial.split('.')[0] : '0'))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.acct {
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 18px;
|
|
||||||
background: @primary;
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 22px 44px -32px rgba(20, 22, 26, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__head {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__eyebrow {
|
|
||||||
font-size: 11px;
|
|
||||||
color: rgba(255, 255, 255, 0.55);
|
|
||||||
padding-bottom: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__figure {
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__unit {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: rgba(255, 255, 255, 0.55);
|
|
||||||
padding-inline-start: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__seal {
|
|
||||||
font-size: 10.5px;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 5px 11px;
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
.acct__seal.is-debtor {
|
|
||||||
color: #fff;
|
|
||||||
background: @wine;
|
|
||||||
}
|
|
||||||
.acct__seal.is-creditor {
|
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.13);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 16px;
|
|
||||||
padding-top: 13px;
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
font-size: 12.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__rowLabel {
|
|
||||||
color: rgba(255, 255, 255, 0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__rowValue {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.acct__rowValue i {
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__actions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__action {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 11px 4px;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 13px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 11.5px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: rgba(255, 255, 255, 0.88);
|
|
||||||
background: rgba(255, 255, 255, 0.07);
|
|
||||||
transition: background 0.18s ease, transform 0.18s ease;
|
|
||||||
}
|
|
||||||
.acct__action:active {
|
|
||||||
transform: scale(0.97);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__invite {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 13px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__inviteIcon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 13px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
color: #fff;
|
|
||||||
background: @wine;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__inviteCopy {
|
|
||||||
flex: 1 1 150px;
|
|
||||||
}
|
|
||||||
.acct__inviteCopy h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.acct__inviteCopy p {
|
|
||||||
margin: 5px 0 0;
|
|
||||||
font-size: 11.5px;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: rgba(255, 255, 255, 0.58);
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct__inviteCta {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
margin-top: 4px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 13px;
|
|
||||||
background: #fff;
|
|
||||||
color: @ink;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
transition: transform 0.18s ease;
|
|
||||||
}
|
|
||||||
.acct__inviteCta:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<!--
|
|
||||||
Ported from legacy henza_pwa/src/components/product/category.vue.
|
|
||||||
Not yet wired to real data: the new API has no categories-with-icon endpoint
|
|
||||||
(Prisma has a Category model, but no controller/route exposes it, and there
|
|
||||||
are no per-category icon images). HomeView passes an empty list for now, so
|
|
||||||
this renders nothing rather than fabricated placeholder tiles — see the
|
|
||||||
"existing issue found" note in the report about the legacy `id === 45`
|
|
||||||
special-case, preserved here for whenever real category data exists.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<router-link :to="{ name: 'search' }" @click="setCats" class="cursor-pointer h-full block">
|
|
||||||
<div
|
|
||||||
class="h-full w-full text-center flex flex-col items-center justify-between rounded-[4px]"
|
|
||||||
:class="value.id === '45' ? 'bg-primary' : 'border border-1 border-secondary'"
|
|
||||||
>
|
|
||||||
<div class="flex justify-center mt-4 shrink-0">
|
|
||||||
<img v-lazy="value.icon?.image" alt="category" class="w-[32px] h-[32px] text-center" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="mb-4 mt-4 py-1 px-1 font-semibold flex-1 flex items-center justify-center text-center min-h-[40px]"
|
|
||||||
:class="value.id === '45' ? 'text-white' : 'text-primary'"
|
|
||||||
>
|
|
||||||
{{ value.title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</router-link>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useFiltersStore } from '../../stores/filtersStore'
|
|
||||||
|
|
||||||
interface CategoryValue {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
icon?: { image: string }
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ value: CategoryValue }>(), {})
|
|
||||||
const filters = useFiltersStore()
|
|
||||||
|
|
||||||
const setCats = () => {
|
|
||||||
filters.query.cats = [props.value.id]
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
<!--
|
|
||||||
Ported from legacy henza_pwa/src/components/slider/homeSlider.vue.
|
|
||||||
`$prefix` (legacy global property, always the compile-time-constant 'montzar'
|
|
||||||
from themeConfig) is inlined directly here rather than re-registered as a
|
|
||||||
global — same resolved value, no behavior change.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Swiper
|
|
||||||
:grabCursor="true"
|
|
||||||
:centeredSlides="true"
|
|
||||||
:space-between="5"
|
|
||||||
:loop="true"
|
|
||||||
:slidesPerView="1.3"
|
|
||||||
class="w-full home-slider"
|
|
||||||
:autoplay="data.autoplay"
|
|
||||||
:pagination="{ clickable: true }"
|
|
||||||
>
|
|
||||||
<SwiperSlide class="bg-center bg-cover h-auto" v-for="(item, index) in slides" :key="index">
|
|
||||||
<img @click="clickHandler(item)" v-lazy="item.src" width="100%" class="block w-full rounded-[10px] object-cover" :style="`height:${height}`" />
|
|
||||||
</SwiperSlide>
|
|
||||||
</Swiper>
|
|
||||||
<Drawer
|
|
||||||
v-model:visible="data.show"
|
|
||||||
placement="bottom"
|
|
||||||
:closable="false"
|
|
||||||
@close="setAutoPlay"
|
|
||||||
height="60%"
|
|
||||||
class="montzar-rounded-drawer rtl text-white bg-primaryLighten whitespace-pre-wrap text[13px]"
|
|
||||||
>
|
|
||||||
{{ data.slideContent.text }}
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
|
||||||
import 'swiper/css'
|
|
||||||
import 'swiper/css/navigation'
|
|
||||||
import 'swiper/css/pagination'
|
|
||||||
import 'swiper/css/scrollbar'
|
|
||||||
import SwiperCore, { Autoplay, Navigation, Pagination } from 'swiper'
|
|
||||||
SwiperCore.use([Pagination, Navigation, Autoplay])
|
|
||||||
import { reactive } from 'vue'
|
|
||||||
import { Drawer } from 'ant-design-vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useFiltersStore } from '../../stores/filtersStore'
|
|
||||||
|
|
||||||
interface Slide {
|
|
||||||
text: string | undefined
|
|
||||||
type: string | undefined
|
|
||||||
src: string | undefined
|
|
||||||
link: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
show: boolean
|
|
||||||
slideContent: Slide
|
|
||||||
autoplay: { delay: number; disableOnInteraction: boolean } | boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<{ slides: Slide[]; height: string }>(), {})
|
|
||||||
|
|
||||||
const data: Data = reactive({
|
|
||||||
show: false,
|
|
||||||
slideContent: { text: undefined, type: undefined, src: undefined, link: undefined },
|
|
||||||
autoplay: { delay: 4000, disableOnInteraction: false },
|
|
||||||
})
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const filters = useFiltersStore()
|
|
||||||
|
|
||||||
const clickHandler = async (item: Slide) => {
|
|
||||||
if (item) {
|
|
||||||
data.slideContent = item
|
|
||||||
switch (item.type) {
|
|
||||||
case 'popup':
|
|
||||||
if (item && !(!item.text || item.text.length === 0)) data.show = true
|
|
||||||
break
|
|
||||||
case 'link':
|
|
||||||
if (item && !(!item.link || item.link.length === 0)) window.open(item.link)?.focus()
|
|
||||||
break
|
|
||||||
case 'discount':
|
|
||||||
if (!(!item.link || item.link.length === 0)) {
|
|
||||||
// EXISTING ISSUE, ported as-is: legacy sets `query.value.discount`,
|
|
||||||
// but the actual filter field is `has_discount` — this assignment
|
|
||||||
// never affected search filtering in legacy either. Not "fixed"
|
|
||||||
// here; flagged separately.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
;(filters.query as any).discount = true
|
|
||||||
await router.push({ name: 'search' })
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
// code block
|
|
||||||
}
|
|
||||||
data.autoplay = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const setAutoPlay = async () => {
|
|
||||||
data.autoplay = { delay: 4000, disableOnInteraction: false }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.home-slider {
|
|
||||||
&.swiper {
|
|
||||||
padding: 24px 0 24px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullet {
|
|
||||||
transition: all 0.2s;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullet-active,
|
|
||||||
.swiper-pagination-bullet-active-main {
|
|
||||||
width: 24px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-horizontal > .swiper-pagination-bullets,
|
|
||||||
.swiper-pagination-bullets.swiper-pagination-horizontal,
|
|
||||||
.swiper-pagination-custom,
|
|
||||||
.swiper-pagination-fraction {
|
|
||||||
bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullets-dynamic * {
|
|
||||||
transform: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullet-active {
|
|
||||||
background: @primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-slide {
|
|
||||||
transition: all 0.6s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-slide:not(.swiper-slide-active) {
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
<!--
|
|
||||||
Ported from legacy henza_pwa/src/components/slider/slider.vue.
|
|
||||||
EXISTING ISSUE, ported as-is (not fixed): `showPopup` takes (src, index) but
|
|
||||||
the template calls it with a single argument (`showPopup(item)`), and the
|
|
||||||
v-for doesn't even destructure an index. `index` is therefore always
|
|
||||||
undefined inside showPopup, so its "rotate the slides array to start at the
|
|
||||||
tapped image" logic never runs — tapping any thumbnail always opens the
|
|
||||||
full-screen zoom viewer starting from the first image, never the tapped one.
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Swiper :grabCursor="true" :centeredSlides="true" :loop="false" :slidesPerView="1" class="w-full slider" :pagination="{ clickable: true }">
|
|
||||||
<SwiperSlide class="bg-center bg-cover h-auto" v-for="item in slides" :key="item">
|
|
||||||
<img @click="showPopup(item)" v-lazy="item" class="block w-auto object-cover z-0 pb-1" :style="`height:${height}`" />
|
|
||||||
</SwiperSlide>
|
|
||||||
<transition name="slideUp" mode="out-in">
|
|
||||||
<svg alt="منتزر" class="absolute bottom-0 z-[2] m-auto pointer-events-none w-full h-[90px]" v-if="flagSvg">
|
|
||||||
<path fill="#FFFFFF" class="w-full h-[90px]" :d="whiteShape" />
|
|
||||||
<path fill="#3E5279" class="w-full h-[90px]" :d="curvedShape" />
|
|
||||||
</svg>
|
|
||||||
</transition>
|
|
||||||
</Swiper>
|
|
||||||
|
|
||||||
<Drawer v-model:visible="show" placement="bottom" :closable="false" :bodyStyle="{ padding: 0 }" class="rtl whitespace-pre-wrap bg-white" height="100vh">
|
|
||||||
<div @click="show = false" class="fixed top-[30px] right-[20px] z-[5] pb-2 pl-5">
|
|
||||||
<Icon icon="vuesax-linear:arrow-right-1" @click="show = false" class="cursor-pointer text-white bg-secondary rounded-[5px] p-[2.5px]" size="28" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Swiper :grabCursor="true" :zoom="{ maxRatio: 1.2 }" :modules="[Zoom]" :centeredSlides="true" :loop="false" :slidesPerView="1" class="w-full slider" :key="show">
|
|
||||||
<SwiperSlide class="bg-center bg-cover h-auto" v-for="item in slides" :key="item">
|
|
||||||
<div class="swiper-zoom-container">
|
|
||||||
<img v-lazy="item" class="block object-contain w-[100vw] h-screen" />
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
</Swiper>
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onBeforeMount, ref } from 'vue'
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
|
||||||
import 'swiper/css'
|
|
||||||
import 'swiper/css/navigation'
|
|
||||||
import 'swiper/css/pagination'
|
|
||||||
import 'swiper/css/scrollbar'
|
|
||||||
import 'swiper/css/zoom'
|
|
||||||
import SwiperCore, { Navigation, Pagination, Zoom } from 'swiper'
|
|
||||||
import { Drawer } from 'ant-design-vue'
|
|
||||||
import Icon from '../icon/Icon.vue'
|
|
||||||
import { useWindowDataStore } from '../../stores/windowDataStore'
|
|
||||||
|
|
||||||
SwiperCore.use([Pagination, Navigation])
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ slides: string[]; height: string; width?: string }>(), {})
|
|
||||||
|
|
||||||
const show = ref(false)
|
|
||||||
const slides = ref<string[]>([...props.slides])
|
|
||||||
|
|
||||||
const showPopup = (src: string, index?: number) => {
|
|
||||||
slides.value = [...props.slides]
|
|
||||||
for (let i = 0; i < (index ?? 0); i++) {
|
|
||||||
const helper = slides.value.shift()
|
|
||||||
if (helper) slides.value.push(helper)
|
|
||||||
}
|
|
||||||
show.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const flagSvg = ref(false)
|
|
||||||
onBeforeMount(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
flagSvg.value = true
|
|
||||||
}, 100)
|
|
||||||
})
|
|
||||||
|
|
||||||
const windowData = useWindowDataStore()
|
|
||||||
const shapeHeight = 90
|
|
||||||
const wcp1x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 21.8878249)
|
|
||||||
const wcp2x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 2.37406336)
|
|
||||||
const crcr1cp1x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 1.56947374)
|
|
||||||
const crcr1cp2x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 5.47609844)
|
|
||||||
const crcr2cp1x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 15.8024691)
|
|
||||||
const crcr2cp2x = computed(() => Math.min(windowData.width, 768) - Math.min(windowData.width, 768) / 2.2606383)
|
|
||||||
const whiteShape = computed(() => `M0,${shapeHeight}H${Math.min(windowData.width, 768)}V0C${wcp1x.value},19.17,${wcp2x.value},119.08,0,49Z`)
|
|
||||||
const curvedShape = computed(
|
|
||||||
() => `M0,39.77C${crcr1cp1x.value},91.74,${crcr1cp2x.value},61.5,${Math.min(windowData.width, 768)},0,C${crcr2cp1x.value},29.34,${crcr2cp2x.value},112.54,0,57.1Z`,
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.slider {
|
|
||||||
.swiper-pagination-bullet {
|
|
||||||
transition: all 0.2s;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullet-active,
|
|
||||||
.swiper-pagination-bullet-active-main {
|
|
||||||
width: 24px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-horizontal > .swiper-pagination-bullets,
|
|
||||||
.swiper-pagination-bullets.swiper-pagination-horizontal,
|
|
||||||
.swiper-pagination-custom,
|
|
||||||
.swiper-pagination-fraction {
|
|
||||||
bottom: 40px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullets-dynamic * {
|
|
||||||
transform: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-pagination-bullet-active {
|
|
||||||
background: @primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiper-slide img {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.slideUp-enter-active,
|
|
||||||
.slideUp-leave-active {
|
|
||||||
opacity: 1;
|
|
||||||
transition: all @transition-duration ease @transition-delay;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slideUp-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slideUp-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!-- Ported from legacy henza_pwa/src/components/content.vue -->
|
|
||||||
<template>
|
|
||||||
<main :style="{ marginTop: transition.transitionEnded ? `${transition.headerHeight}px` : 'unset' }">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useTransitionStore } from '../../stores/transitionStore'
|
|
||||||
|
|
||||||
const transition = useTransitionStore()
|
|
||||||
</script>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<!-- Ported from legacy henza_pwa/src/components/ui/ScrollFetch.vue — the plain
|
|
||||||
infinite-scroll container (default ant Empty, no custom illustration). -->
|
|
||||||
<template>
|
|
||||||
<main
|
|
||||||
class="overflow-auto no-scrollbar w-[100vw] h-screen max-w-screen-md m-auto"
|
|
||||||
:style="{ paddingTop: padding || transition.headerHeight + 'px' }"
|
|
||||||
ref="scrollArea"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
<Spinner cls="my-[10px]" :spinning="loading" :indicator="true" tip="درحال دریافت اطلاعات" />
|
|
||||||
<EmptyState v-if="flag" />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useInfiniteScroll } from '@vueuse/core'
|
|
||||||
import Spinner from '../Spinner.vue'
|
|
||||||
import EmptyState from '../EmptyState.vue'
|
|
||||||
import { useTransitionStore } from '../../stores/transitionStore'
|
|
||||||
|
|
||||||
withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
loading: boolean
|
|
||||||
flag: boolean
|
|
||||||
padding?: string
|
|
||||||
}>(),
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
const emit = defineEmits<{ (e: 'fetch'): void }>()
|
|
||||||
const transition = useTransitionStore()
|
|
||||||
const scrollArea = ref(null)
|
|
||||||
|
|
||||||
useInfiniteScroll(
|
|
||||||
scrollArea,
|
|
||||||
() => {
|
|
||||||
emit('fetch')
|
|
||||||
},
|
|
||||||
{ distance: 50 },
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<!--
|
|
||||||
Ported from legacy henza_pwa/src/components/ui/ScrollFetchAfterSearch.vue.
|
|
||||||
Legacy imported emp_after_search.svg / emp_before_search.svg and an `isSearch`
|
|
||||||
prop to pick between them, but the live template used neither — it rendered
|
|
||||||
`ordersEmpty.svg` unconditionally. Ported as-is (unused imports dropped,
|
|
||||||
`isSearch` kept since it is part of the component's declared prop contract).
|
|
||||||
-->
|
|
||||||
<template>
|
|
||||||
<main
|
|
||||||
class="overflow-auto no-scrollbar w-[100vw] h-screen max-w-screen-md m-auto"
|
|
||||||
:style="{ paddingTop: padding || transition.headerHeight + 'px' }"
|
|
||||||
ref="scrollArea"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
<Spinner cls="my-[10px]" :spinning="loading" :indicator="true" tip="درحال دریافت اطلاعات" />
|
|
||||||
<EmptyState v-if="flag && !loading" :image="ordersEmpty" />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useInfiniteScroll } from '@vueuse/core'
|
|
||||||
import Spinner from '../Spinner.vue'
|
|
||||||
import EmptyState from '../EmptyState.vue'
|
|
||||||
import ordersEmpty from '../../assets/img/empty/ordersEmpty.svg'
|
|
||||||
import { useTransitionStore } from '../../stores/transitionStore'
|
|
||||||
|
|
||||||
withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
loading: boolean
|
|
||||||
flag: boolean
|
|
||||||
padding?: string
|
|
||||||
isSearch?: boolean
|
|
||||||
}>(),
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
const emit = defineEmits<{ (e: 'fetch'): void }>()
|
|
||||||
const transition = useTransitionStore()
|
|
||||||
const scrollArea = ref(null)
|
|
||||||
|
|
||||||
useInfiniteScroll(
|
|
||||||
scrollArea,
|
|
||||||
() => {
|
|
||||||
emit('fetch')
|
|
||||||
},
|
|
||||||
{ distance: 50 },
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
// Ported from legacy henza_pwa/src/store/loadingStore.ts — a single global
|
|
||||||
// loading flag, converted from the hand-rolled ref-class to Pinia.
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
|
|
||||||
export const useLoadingStore = defineStore('loading', {
|
|
||||||
state: () => ({
|
|
||||||
loading: false,
|
|
||||||
}),
|
|
||||||
actions: {
|
|
||||||
setLoading(loading: boolean) {
|
|
||||||
this.loading = loading
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -11,20 +11,6 @@ export function price(val: number | string | undefined | null): string {
|
|||||||
return val as any
|
return val as any
|
||||||
}
|
}
|
||||||
|
|
||||||
export function priceUnit(val: number | string | undefined | null): string {
|
|
||||||
if (['string', 'number'].includes(typeof val)) {
|
|
||||||
return `${val!.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')} تومان `
|
|
||||||
}
|
|
||||||
return val as any
|
|
||||||
}
|
|
||||||
|
|
||||||
export function priceUnitRialLess(val: number | string | undefined | null): string {
|
|
||||||
if (['string', 'number'].includes(typeof val)) {
|
|
||||||
return `${val!.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')} `
|
|
||||||
}
|
|
||||||
return val as any
|
|
||||||
}
|
|
||||||
|
|
||||||
export function jdate(val: string | undefined | null): string {
|
export function jdate(val: string | undefined | null): string {
|
||||||
if (val) {
|
if (val) {
|
||||||
// legacy parsed a fixed 'YYYY-MM-DD HH:mm:ss' Laravel timestamp format;
|
// legacy parsed a fixed 'YYYY-MM-DD HH:mm:ss' Laravel timestamp format;
|
||||||
|
|||||||
@@ -1,30 +1,9 @@
|
|||||||
// Ported from legacy henza_pwa/src/Mixins/validator/index.ts — same rules,
|
// Ported from legacy henza_pwa/src/Mixins/validator/index.ts — same rules,
|
||||||
// same messages, verbatim.
|
// same messages, verbatim. Only the rules the app uses are kept (DEAD-01/02).
|
||||||
export const message = (field: string) => ` مقدار ${field} نا معتبر است `
|
export const message = (field: string) => ` مقدار ${field} نا معتبر است `
|
||||||
|
|
||||||
export const mobile = (field: string) => ({
|
export const mobile = (field: string) => ({
|
||||||
pattern: /^(\+98|0098|98|0)?9\d{9}$/g,
|
pattern: /^(\+98|0098|98|0)?9\d{9}$/g,
|
||||||
message: message(field),
|
message: message(field),
|
||||||
})
|
})
|
||||||
export const phone = (field: string) => ({
|
|
||||||
pattern: /^((\+98|0098|98|0)\d1)?\d{8}$/g,
|
|
||||||
message: message(field),
|
|
||||||
})
|
|
||||||
export const fa = (field: string) => ({
|
|
||||||
pattern: /^([-ۿﮊپچگ ])+$/,
|
|
||||||
message: ` مقدار ${field} باید فارسی باشد `,
|
|
||||||
})
|
|
||||||
export const mail = (field: string) => ({ type: 'email', message: message(field) })
|
|
||||||
export const url = (field: string) => ({ type: 'url', message: message(field) })
|
|
||||||
export const username = (field: string) => ({
|
|
||||||
pattern: /^(?!.*\.\.)(?!.*\.$)[^\W][\w.]{0,29}$/gim,
|
|
||||||
message: ` مقدار ${field} باید شامل حروف و اعداد انگلیسی و _ باشد `,
|
|
||||||
})
|
|
||||||
export const required = (field: string) => ({ required: true, message: ` لطفا ${field} را وارد کنید ` })
|
export const required = (field: string) => ({ required: true, message: ` لطفا ${field} را وارد کنید ` })
|
||||||
export const sheba = (field: string) => ({ pattern: /^IR\d{24}$/gim, message: message(field) })
|
|
||||||
export const card = (field: string) => ({ pattern: /^\d{16}$/gim, message: message(field) })
|
|
||||||
export const zip = (field: string) => ({ pattern: /^\d{10}$/gim, message: message(field) })
|
|
||||||
export const num = (field: string) => ({ pattern: /^\d*$/gim, message: message(field) })
|
|
||||||
export const len = (field: string, len: number) => ({ len, message: ` مقدار ${field} باید ${len} کارکتر باشد ` })
|
|
||||||
export const max = (field: string, max: number) => ({ max, message: ` مقدار ${field} باید کمتر از ${max} کارکتر باشد ` })
|
|
||||||
export const min = (field: string, min: number) => ({ min, message: ` مقدار ${field} باید بیشتر از ${min} کارکتر باشد ` })
|
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium`
|
|||||||
|
|
||||||
| ID | Severity | Where | Description |
|
| ID | Severity | Where | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| DEAD-01 | Low | TGT `apps/user-app` | 8 components with no importers, `loadingStore`, `api.getBalance` (zero call sites), unused `gsap` dependency. |
|
| DEAD-01 | Low | TGT `apps/user-app` | 8 components with no importers, `loadingStore`, `api.getBalance` (zero call sites), unused `gsap` dependency. ✅ **FIXED 2026-10-08** (owner approved): removed 8 unreachable components + loadingStore (found by walking imports from main.ts; sw.ts kept — vite-plugin-pwa entry), the unused validators/price helpers, and the gsap + @ant-design/icons-vue dependencies. getBalance was already gone. |
|
||||||
| DEAD-02 | Low | TGT `apps/user-app-v2` | 11 dead components, `loadingStore`, `getBalance`, `priceUnit`/`priceUnitRialLess`, 12 of 15 validators, unused `gsap` + `@ant-design/icons-vue` deps. |
|
| DEAD-02 | Low | TGT `apps/user-app-v2` | 11 dead components, `loadingStore`, `getBalance`, `priceUnit`/`priceUnitRialLess`, 12 of 15 validators, unused `gsap` + `@ant-design/icons-vue` deps. ✅ **FIXED 2026-10-08** (owner approved): same for v2: 11 unreachable components + loadingStore, unused validators/price helpers, gsap + @ant-design/icons-vue. |
|
||||||
| DEAD-03 | Low | TGT `gold-banking-monorepo/user-app-v2/` (repo root, outside `apps/`) | Stray leftover folder containing only `package.json` and an **older** `vite.config.ts` (hardcoded port 5179, hardcoded proxy, imports a `themeConfig` that doesn't exist there). Not matched by `pnpm-workspace.yaml`, so pnpm ignores it. Left in place — deleting it needs user approval. |
|
| DEAD-03 | Low | TGT `gold-banking-monorepo/user-app-v2/` (repo root, outside `apps/`) | Stray leftover folder containing only `package.json` and an **older** `vite.config.ts` (hardcoded port 5179, hardcoded proxy, imports a `themeConfig` that doesn't exist there). Not matched by `pnpm-workspace.yaml`, so pnpm ignores it. Left in place — deleting it needs user approval. ✅ **FIXED 2026-10-08** (owner approved): stray root `user-app-v2/` deleted; nothing referenced it. |
|
||||||
| DEAD-04 | Low | REF `henza_server/app/Console/Kernel.php` | `schedule()` is **empty** — no cron. Price refresh / expiry / birthday logic must be driven by OS cron or the `GET /` web route. |
|
| DEAD-04 | Low | REF `henza_server/app/Console/Kernel.php` | `schedule()` is **empty** — no cron. Price refresh / expiry / birthday logic must be driven by OS cron or the `GET /` web route. |
|
||||||
| DEAD-05 | Low | REF `henza_server` | PayPing `returnUrl` points at `site/transaction/verify`, which is **not registered in any route file** — the payment callback is unreachable. |
|
| DEAD-05 | Low | REF `henza_server` | PayPing `returnUrl` points at `site/transaction/verify`, which is **not registered in any route file** — the payment callback is unreachable. |
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-19
@@ -132,7 +132,7 @@ importers:
|
|||||||
version: 10.4.9(esbuild@0.25.12)
|
version: 10.4.9(esbuild@0.25.12)
|
||||||
'@nestjs/testing':
|
'@nestjs/testing':
|
||||||
specifier: ^10.4.22
|
specifier: ^10.4.22
|
||||||
version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/platform-express@10.4.22)
|
version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22))
|
||||||
'@types/bcryptjs':
|
'@types/bcryptjs':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
@@ -178,9 +178,6 @@ importers:
|
|||||||
|
|
||||||
apps/user-app:
|
apps/user-app:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/icons-vue':
|
|
||||||
specifier: ^6.1.0
|
|
||||||
version: 6.1.0(vue@3.5.42(typescript@5.9.3))
|
|
||||||
'@gold-bank/shared-types':
|
'@gold-bank/shared-types':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/shared-types
|
version: link:../../packages/shared-types
|
||||||
@@ -193,9 +190,6 @@ importers:
|
|||||||
ant-design-vue:
|
ant-design-vue:
|
||||||
specifier: ^3.2.20
|
specifier: ^3.2.20
|
||||||
version: 3.2.20(vue@3.5.42(typescript@5.9.3))
|
version: 3.2.20(vue@3.5.42(typescript@5.9.3))
|
||||||
gsap:
|
|
||||||
specifier: ^3.9.1
|
|
||||||
version: 3.15.0
|
|
||||||
moment-jalaali:
|
moment-jalaali:
|
||||||
specifier: ^0.10.0
|
specifier: ^0.10.0
|
||||||
version: 0.10.5
|
version: 0.10.5
|
||||||
@@ -254,9 +248,6 @@ importers:
|
|||||||
|
|
||||||
apps/user-app-v2:
|
apps/user-app-v2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/icons-vue':
|
|
||||||
specifier: ^6.1.0
|
|
||||||
version: 6.1.0(vue@3.5.42(typescript@5.9.3))
|
|
||||||
'@gold-bank/shared-types':
|
'@gold-bank/shared-types':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/shared-types
|
version: link:../../packages/shared-types
|
||||||
@@ -269,9 +260,6 @@ importers:
|
|||||||
ant-design-vue:
|
ant-design-vue:
|
||||||
specifier: ^3.2.20
|
specifier: ^3.2.20
|
||||||
version: 3.2.20(vue@3.5.42(typescript@5.9.3))
|
version: 3.2.20(vue@3.5.42(typescript@5.9.3))
|
||||||
gsap:
|
|
||||||
specifier: ^3.9.1
|
|
||||||
version: 3.15.0
|
|
||||||
moment-jalaali:
|
moment-jalaali:
|
||||||
specifier: ^0.10.0
|
specifier: ^0.10.0
|
||||||
version: 0.10.5
|
version: 0.10.5
|
||||||
@@ -3343,9 +3331,6 @@ packages:
|
|||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
gsap@3.15.0:
|
|
||||||
resolution: {integrity: sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==}
|
|
||||||
|
|
||||||
handlebars@4.7.9:
|
handlebars@4.7.9:
|
||||||
resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==}
|
resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==}
|
||||||
engines: {node: '>=0.4.7'}
|
engines: {node: '>=0.4.7'}
|
||||||
@@ -6762,7 +6747,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- chokidar
|
- chokidar
|
||||||
|
|
||||||
'@nestjs/testing@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/platform-express@10.4.22)':
|
'@nestjs/testing@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -8790,8 +8775,6 @@ snapshots:
|
|||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
gsap@3.15.0: {}
|
|
||||||
|
|
||||||
handlebars@4.7.9:
|
handlebars@4.7.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "user-app-v2",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vue-tsc -b && vite build",
|
|
||||||
"lint": "eslint \"src/**/*.{ts,vue}\""
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@ant-design/icons-vue": "^6.1.0",
|
|
||||||
"@gold-bank/shared-types": "workspace:*",
|
|
||||||
"@vueuse/core": "^9.13.0",
|
|
||||||
"ant-design-vue": "^3.2.20",
|
|
||||||
"gsap": "^3.9.1",
|
|
||||||
"moment-jalaali": "^0.10.0",
|
|
||||||
"pinia": "^2.2.0",
|
|
||||||
"swiper": "^7.4.1",
|
|
||||||
"vant": "^4.0.3",
|
|
||||||
"vue": "^3.5.0",
|
|
||||||
"vue-router": "^4.4.0",
|
|
||||||
"vue3-lazyload": "^0.2.5-beta",
|
|
||||||
"@lottiefiles/dotlottie-vue": "^0.4.15"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitejs/plugin-vue": "^5.1.0",
|
|
||||||
"autoprefixer": "^10.4.2",
|
|
||||||
"less": "^4.1.2",
|
|
||||||
"postcss": "^8.4.5",
|
|
||||||
"tailwindcss": "3.0.18",
|
|
||||||
"typescript": "^5.5.0",
|
|
||||||
"vite": "^5.4.0",
|
|
||||||
"vue-tsc": "^2.1.0",
|
|
||||||
"vite-plugin-pwa": "^0.20.5",
|
|
||||||
"workbox-precaching": "^7.1.0",
|
|
||||||
"workbox-window": "^7.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import vue from '@vitejs/plugin-vue';
|
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
|
||||||
import { $themeLessVars } from './themeConfig/themeConfig';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
vue(),
|
|
||||||
// Ported from legacy vite.config.ts:18-52. Same injectManifest strategy,
|
|
||||||
// same src/sw.ts entry, same manifest values (name/colors/icons/display).
|
|
||||||
VitePWA({
|
|
||||||
base: '/',
|
|
||||||
srcDir: 'src',
|
|
||||||
filename: 'sw.ts',
|
|
||||||
strategies: 'injectManifest',
|
|
||||||
includeAssets: ['favicon.ico'],
|
|
||||||
// legacy ran the SW in dev too (mode: "development")
|
|
||||||
devOptions: { enabled: true, type: 'module' },
|
|
||||||
manifest: {
|
|
||||||
name: 'Montzar',
|
|
||||||
short_name: 'Montzar',
|
|
||||||
description: 'Montzar',
|
|
||||||
theme_color: $themeLessVars.primary,
|
|
||||||
background_color: $themeLessVars.primary,
|
|
||||||
start_url: '/',
|
|
||||||
display: 'standalone',
|
|
||||||
icons: [
|
|
||||||
{ src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
|
|
||||||
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
|
|
||||||
{ src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
server: {
|
|
||||||
host: '0.0.0.0',
|
|
||||||
port: 5179,
|
|
||||||
proxy: {
|
|
||||||
'/api': 'http://localhost:3000',
|
|
||||||
'/uploads': 'http://localhost:3000',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
preprocessorOptions: {
|
|
||||||
less: {
|
|
||||||
modifyVars: $themeLessVars,
|
|
||||||
javascriptEnabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user