SearchView: Removed the dynamic collection-tile grid (e.g. "پرفروش‌های این هفته") and the "نتایج / N محصول" section header from the UI. Underlying data/filtering logic untouched.

DashboardView ("میز کار"): Rebuilt from scratch with a new structure, distinct from the old card+grid layout but visually aligned with HomeView (same blurred bar, @ink/@wine palette, staged reveal animation on mount):

Single hero card (ink→wine gradient) showing balance, gold/fiat debt, and identity — no quick-action icons inside it anymore.

Live gold rate folded into a slim inline row at the bottom instead of a titled section.
Iterated shadow/spacing on the hero card per feedback (softer, less clipped).
DialogVant / DialogVantOneButton (shared modal components, used in 7+ call sites: Dashboard, Cart, SingleProduct, Register, App): Replaced Vant's default dialog chrome with the app's own rounded-card language — circular icon badge for the illustration, pill-shaped primary/secondary buttons. No call-site markup changed.

LoginView: Rebuilt from scratch — full-bleed dark hero (brand + step copy) overlapped by a floating white form sheet, animated crossfade transition between the mobile-number and OTP steps, same staged entrance animation as Home/Dashboard. All auth logic (API check, OTP stub, session login) unchanged.
This commit is contained in:
s.esmaeili
2026-09-18 12:54:45 +03:30
parent 05fe780c89
commit 53a304b5d5
355 changed files with 22865 additions and 14 deletions
+101
View File
@@ -0,0 +1,101 @@
// Ported from legacy henza_pwa/src/store/cartStore.ts, adapted to our string
// ids and flat (non-paginated) product list. Persisted the same way (a single
// localStorage key), just via Pinia's own state instead of @vueuse's
// useStorage wrapping a hand-rolled class.
import { defineStore } from 'pinia'
import { api, type ProductSummary } from '../api/client'
export interface CartItem {
pid: string
vid: string
product: ProductSummary
qty: number
note?: string | null
}
const STORAGE_KEY = 'appCart'
function loadFromStorage(): CartItem[] {
try {
const raw = localStorage.getItem(STORAGE_KEY)
return raw ? JSON.parse(raw) : []
} catch {
return []
}
}
function persist(items: CartItem[]) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items))
}
export const useCartStore = defineStore('cart', {
state: () => ({
items: loadFromStorage() as CartItem[],
}),
actions: {
// Re-syncs cart entries against fresh product data (e.g. price/weight
// changes since the item was added), dropping entries whose
// product/variant no longer exists — same intent as legacy's init().
async init() {
try {
const products = await api.listProducts()
const refreshed: CartItem[] = []
for (const item of this.items) {
const product = products.find((p) => p.id === item.pid)
const variant = product?.variants.find((v) => v.id === item.vid)
if (product && variant) {
refreshed.push({ ...item, product })
}
}
this.items = refreshed
persist(this.items)
} catch (e) {
console.log(e)
}
},
addToCart(data: CartItem) {
const index = this.find_index(data)
if (index === -1) this.items.push(data)
else {
this.items[index].product = data.product
this.items[index].qty = data.qty
}
persist(this.items)
},
remove(item: CartItem) {
const index = this.find_index(item)
if (index > -1) this.items.splice(index, 1)
persist(this.items)
},
// Legacy's exact name (typo'd "Feild"), kept for consistency with the rest
// of this faithfully-ported store's method names.
updateFeild(value: unknown, index: number | null, item: Pick<CartItem, 'pid' | 'vid'>, field: keyof CartItem) {
if (!this.isInCart(item)) return false
const i = index ?? this.find_index(item)
;(this.items[i] as any)[field] = value
persist(this.items)
return true
},
find_index(item: Pick<CartItem, 'pid' | 'vid'>) {
return this.items.findIndex((cartItem) => cartItem.pid === item.pid && cartItem.vid === item.vid)
},
isInCart(item: Pick<CartItem, 'pid' | 'vid'>) {
return this.find_index(item) > -1
},
variant(item: CartItem) {
return item.product.variants.find((v) => v.id === item.vid)
},
total() {
return this.items
.reduce((sum, item) => {
const v = this.variant(item)
return sum + (v ? parseFloat(v.rashWeightGrams) * item.qty : 0)
}, 0)
.toFixed(2)
},
clear() {
this.items = []
persist(this.items)
},
},
})
@@ -0,0 +1,93 @@
// Scoped port of legacy henza_pwa/src/store/store.ts.
//
// Legacy fetched one curated payload (POST /front + GET /config) that carried
// the home banner slider, the category grid, promotions, the ignore-list and
// the gold price ticker. The new API exposes none of that as a single config
// endpoint, so this store restores the two pieces that drive visible home-page
// UI and are actually derivable today:
//
// - `cats` — REAL data: distinct categories derived from GET /catalog/products
// (the response already includes each product's category).
// Legacy's per-category `icon.image` was a remote URL from the old
// Laravel host and has no equivalent, so a local brand mark stands
// in until category artwork exists.
// - `config.main_slider` — legacy's banner slides were remote URLs too. These
// are placeholder slides built from legacy's own local artwork,
// using legacy's exact slide shape ({text, type, src, link}) so
// HomeSlider's popup/link/discount click handling works unchanged.
//
// Not ported (no backend): promotionList, ignoreList, priceList, isLoaded.
import { defineStore } from 'pinia'
import { api } from '../api/client'
import shippingArt from '../assets/img/shipping-and-delivery.png'
import competitionArt from '../assets/img/sports-and-competition.png'
import brandArt from '../assets/img/montzar-logo.png'
export interface Slide {
text: string | undefined
type: string | undefined
src: string | undefined
link: string | undefined
}
export interface HomeCategory {
id: string
title: string
icon?: { image: string }
}
const PLACEHOLDER_SLIDES: Slide[] = [
{
type: 'popup',
src: shippingArt,
text: 'ارسال سریع و مطمئن سفارش‌های شما به سراسر کشور.',
link: undefined,
},
{
type: 'popup',
src: competitionArt,
text: 'جدیدترین محصولات منتزر را در بخش محصولات جدید ببینید.',
link: undefined,
},
{
type: 'popup',
src: brandArt,
text: 'منتزر، همراه شما در خرید و فروش طلا.',
link: undefined,
},
]
export const useConfigStore = defineStore('config', {
state: () => ({
config: { main_slider: PLACEHOLDER_SLIDES as Slide[] },
cats: [] as HomeCategory[],
loaded: false,
}),
actions: {
setCats(cats: HomeCategory[]) {
this.cats = cats
},
// Legacy's store.init() fanned out to /config, /promotion, /ignore, /price.
// Here the only live source is the product catalogue, from which the real
// category list is derived.
async init() {
try {
const products = await api.listProducts()
const seen = new Map<string, HomeCategory>()
for (const p of products) {
if (p.category && !seen.has(p.category.id)) {
seen.set(p.category.id, {
id: p.category.id,
title: p.category.title,
icon: { image: brandArt },
})
}
}
this.cats = [...seen.values()]
this.loaded = true
} catch (e) {
console.log(e)
}
},
},
})
@@ -0,0 +1,22 @@
// Adapted from legacy henza_pwa/src/Mixins/favorite.ts + the `favorits` slice of
// myDataStore. Legacy persisted favorites server-side via POST /add-to-fav; the
// new API has no favorites endpoint yet, so this is local-only (Pinia state,
// not persisted) until that backend support exists. Flagged — not a silent
// behavior match, a deliberate stand-in.
import { defineStore } from 'pinia'
export const useFavoritesStore = defineStore('favorites', {
state: () => ({
ids: [] as string[],
}),
actions: {
toggle(id: string) {
const index = this.ids.indexOf(id)
if (index === -1) this.ids.push(id)
else this.ids.splice(index, 1)
},
isFavorite(id: string) {
return this.ids.includes(id)
},
},
})
@@ -0,0 +1,40 @@
// Ported from legacy henza_pwa/src/store/filtersStore.ts — same field shape.
// `cats`, `brands`, `limit_gram`, and `text` are wired to real filtering in
// SearchView/Filters.vue. `colors` and `limit_percent` stay unused — see
// Filters.vue for why (no color data, wage-range filter was banker-only).
// `ciny_id`/`has_discount` are also unused (collections use a dedicated
// route param instead; no promotions data exists).
import { defineStore } from 'pinia'
export interface FiltersQuery {
colors: string[]
brands: string[]
ciny_id: string[]
limit_percent: number[]
limit_gram: number[]
has_discount: boolean
text?: string
cats?: string[]
page: number
}
const defaultQuery: FiltersQuery = {
colors: [],
brands: [],
ciny_id: [],
limit_percent: [],
limit_gram: [],
has_discount: false,
text: undefined,
cats: undefined,
page: 0,
}
export const useFiltersStore = defineStore('filters', {
state: () => ({ query: { ...defaultQuery } as FiltersQuery }),
actions: {
reset() {
this.query = { ...defaultQuery }
},
},
})
@@ -0,0 +1,14 @@
// 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
},
},
})
+47
View File
@@ -0,0 +1,47 @@
// Ported from legacy henza_pwa/src/store/navStore.ts (a hand-rolled reactive
// class) to a real Pinia store, preserving the same state shape and setter
// behavior. The route list itself was redesigned to a 5-tab bottom nav
// (Home, Search, Cart, Orders, Profile) — see components/ui/Navigation.vue.
import { defineStore } from 'pinia'
interface NavItem {
name: string
path: string
icon: string
label: string
index: number
}
interface NavState {
navBarRoutes: NavItem[]
activeLink: number
showNav: boolean
// legacy passes `undefined` to clear the transition (router/index.ts:270)
pageTransition: string | undefined
}
export const useNavStore = defineStore('nav', {
state: (): NavState => ({
navBarRoutes: [
{ name: 'home', path: '/home', icon: 'vuesax-linear:home-2', label: 'خانه', index: 1 },
{ name: 'search', path: '/search', icon: 'vuesax-linear:search-normal-1', label: 'جستجو', index: 2 },
{ name: 'cart', path: '/cart', icon: 'vuesax-linear:bag-happy', label: 'سبد خرید', index: 3 },
{ name: 'ordersList', path: '/orders', icon: 'vuesax-linear:receipt-text', label: 'سفارشات', index: 4 },
{ name: 'userPage::hasSubRoute', path: '/user', icon: 'vuesax-linear:user', label: 'پروفایل', index: 5 },
],
activeLink: 1,
showNav: true,
pageTransition: 'right',
}),
actions: {
setNavVisibility(data: boolean) {
this.showNav = data
},
setActiveLink(activeLink: number) {
this.activeLink = activeLink
},
setPgaTransition(transition: string | undefined) {
this.pageTransition = transition
},
},
})
+35
View File
@@ -0,0 +1,35 @@
import { defineStore } from 'pinia';
/**
* Holds the logged-in customer's identity. Now backed by a real (if stubbed —
* see LoginView.vue) login flow: `login()` sets the same `appToken`
* localStorage key legacy's `isUserLoggedIn()` reads, so downstream login
* gates keep working unchanged.
*/
export const useSessionStore = defineStore('session', {
state: () => ({
customerId: localStorage.getItem('gold-bank:customerId') as string | null,
mobile: localStorage.getItem('gold-bank:mobile') as string | null,
}),
actions: {
setCustomerId(id: string) {
this.customerId = id;
localStorage.setItem('gold-bank:customerId', id);
},
login(customerId: string, mobile: string) {
this.customerId = customerId;
this.mobile = mobile;
localStorage.setItem('gold-bank:customerId', customerId);
localStorage.setItem('gold-bank:mobile', mobile);
// matches legacy's isUserLoggedIn() check (Mixins/checkAccess.ts)
localStorage.setItem('appToken', customerId);
},
logout() {
this.customerId = null;
this.mobile = null;
localStorage.removeItem('gold-bank:customerId');
localStorage.removeItem('gold-bank:mobile');
localStorage.removeItem('appToken');
},
},
});
@@ -0,0 +1,27 @@
// Ported from legacy henza_pwa/src/store/transitionStore.ts.
import { defineStore } from 'pinia'
interface TransitionState {
transitionFlag: boolean
transitionEnded: boolean
headerHeight: number
}
export const useTransitionStore = defineStore('transition', {
state: (): TransitionState => ({
transitionFlag: true,
transitionEnded: true,
headerHeight: 0,
}),
actions: {
setTransitionFlag(transitionFlag: boolean) {
this.transitionFlag = transitionFlag
},
setTransitionEnded(transitionEnded: boolean) {
this.transitionEnded = transitionEnded
},
setHeaderHeight(height: number) {
this.headerHeight = height
},
},
})
@@ -0,0 +1,22 @@
// Ported from legacy henza_pwa/src/store/windowData.ts.
import { defineStore } from 'pinia'
interface WindowDataState {
width: number
pullingDown: boolean
}
export const useWindowDataStore = defineStore('windowData', {
state: (): WindowDataState => ({
width: 0,
pullingDown: false,
}),
actions: {
setWidth(width: number) {
this.width = width
},
setPullingDown(pulling: boolean) {
this.pullingDown = pulling
},
},
})