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:
@@ -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
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user