feat(home): landing-style live chart, dynamic product tabs, and price copy

Replace home chart with landing live card, catalog-driven tabs, and smoother range transitions; normalize مظنه to قیمت in user-facing strings; add installment and zaarbox chart APIs.
This commit is contained in:
2026-10-21 03:39:00 -04:00
parent 7626453def
commit 2048680d7a
33 changed files with 1498 additions and 91 deletions
+2
View File
@@ -10,6 +10,7 @@ import boursecuAssetsIcon from '@/assets/img/boursecu-assets-icon.png'
import boursecuHomeIcon from '@/assets/img/boursecu-home-icon.png'
import boursecuSettingsIcon from '@/assets/img/boursecu-settings-icon.png'
import boursecuSupportIcon from '@/assets/img/boursecu-support-icon.png'
import boursecuAboutIcon from '@/assets/img/boursecu-about-icon.png'
import boursecuPriceTriggerIcon from '@/assets/img/boursecu-price-trigger-icon.png'
import boursecuRulesIcon from '@/assets/img/boursecu-rules-icon.png'
@@ -81,6 +82,7 @@ export function getBoursecuActionIconSrc(actionId: string): string | undefined {
home: boursecuHomeIcon,
settings: boursecuSettingsIcon,
support: boursecuSupportIcon,
about: boursecuAboutIcon,
priceTrigger: boursecuPriceTriggerIcon,
rules: boursecuRulesIcon,
}
+1 -1
View File
@@ -62,7 +62,7 @@ export function shouldNormalizeInputDigits(_options?: {
type?: string
inputmode?: string
}): boolean {
// همیشه ارقام فارسی/عربی → انگلیسی (لاگین، گرم، تعداد، مظنه، …)
// همیشه ارقام فارسی/عربی → انگلیسی (لاگین، گرم، تعداد، قیمت، …)
return true
}
+25
View File
@@ -0,0 +1,25 @@
/** جایگزینی «مظنه» و شکل‌های وابسته با «قیمت» در متن‌های نمایشی به مشتری */
const MAZANEH_TO_PRICE: Array<[RegExp, string]> = [
[/مظنه‌های/g, 'قیمت‌های'],
[/مظنه‌ها/g, 'قیمت‌ها'],
[/مظنه‌ای/g, 'قیمتی'],
[/مظنهای/g, 'قیمتی'],
[/مظنهٔ/g, 'قیمت'],
[/مظنه/g, 'قیمت'],
]
export function replaceMazanehTerminology(text: string): string {
let result = text
for (const [pattern, replacement] of MAZANEH_TO_PRICE) {
result = result.replace(pattern, replacement)
}
return result
}
export function polishUserFacingCopy(text: unknown): string {
if (typeof text !== 'string') return ''
const trimmed = text.trim()
if (!trimmed) return ''
return replaceMazanehTerminology(trimmed)
}
+3 -3
View File
@@ -18,11 +18,11 @@ let lastSubscribers = 0
/** @deprecated از lastPrice استفاده کنید — برای سازگاری با کد قدیمی sync می‌شود */
export const price = ref<Price>()
export const lastPrice = ref<Price>()
/** حداقل یک بار تلاش برای گرفتن مظنه تمام شده (موفق یا ناموفق) */
/** حداقل یک بار تلاش برای گرفتن قیمت تمام شده (موفق یا ناموفق) */
export const lastPriceReady = ref(false)
/**
* بعد از اولین پاسخ خالی/no_quote، چند لحظه صبر می‌کنیم تا تیک SSE/زنده برسد
* و «مظنه در دسترس نیست» زودهنگام نشان داده نشود.
* و «قیمت در دسترس نیست» زودهنگام نشان داده نشود.
*/
export const lastPriceSettleGeneration = ref(0)
export const olderPrice = ref<number | undefined>(0)
@@ -92,7 +92,7 @@ function markLastPriceReady() {
scheduleQuoteAbsenceSettle()
}
/** قیمت آمده، بسته قطعی است، یا پنجرهٔ انتظارِ مظنه خالی تمام شده */
/** قیمت آمده، بسته قطعی است، یا پنجرهٔ انتظارِ قیمت خالی تمام شده */
export function isLastPriceAbsenceSettled(): boolean {
void lastPriceSettleGeneration.value
if (!lastPriceReady.value) return false
+1 -1
View File
@@ -38,7 +38,7 @@ const STREAM_CONNECTING = 0
const STREAM_OPEN = 1
const STREAM_CLOSED = 2
/** fetch + ReadableStream — با توکن اختلاف قیمت ویژه؛ بدون توکن همان مظنهٔ عمومی (مهمان). */
/** fetch + ReadableStream — با توکن اختلاف قیمت ویژه؛ بدون توکن همان قیمت عمومی (مهمان). */
class AuthPriceStream extends EventTarget {
readyState = STREAM_CONNECTING
onopen: (() => void) | null = null
+6 -5
View File
@@ -1,6 +1,7 @@
import { ref } from 'vue'
import { useSettingsStore } from '@/store/settingsStore'
import { isLastPriceAbsenceSettled, lastPrice, lastPriceReady } from '@/utils/price'
import { replaceMazanehTerminology } from '@/utils/persianTerminology'
export interface StoreTradingHours {
start: string
@@ -253,7 +254,7 @@ export function isStoreClosed(): boolean {
export const STORE_CLOSED_TITLE = 'فروشگاه بسته است'
export function stripHtmlMessage(message: string): string {
return message.replace(/<[^>]*>/g, '').trim()
return replaceMazanehTerminology(message.replace(/<[^>]*>/g, '').trim())
}
export function getStoreClosedHint(customMessage?: unknown): string {
@@ -283,7 +284,7 @@ export function shouldBlockStoreTrading(): boolean {
export type PriceDisplayState = 'open' | 'closed' | 'no_quote'
export const NO_QUOTE_MESSAGE = 'مظنه در دسترس نیست'
export const NO_QUOTE_MESSAGE = 'قیمت در دسترس نیست'
/** سوییچ: خارج از ساعت کاری هم قیمت صفحه اصلی نشان داده شود */
export function shouldShowHomePriceWhenClosed(): boolean {
@@ -294,7 +295,7 @@ export function shouldShowLastSavedPrice(): boolean {
return useSettingsStore().isSettingTruthy('show_last_price')
}
/** فقط این منابع «آخرین قیمت ذخیره‌شده» هستند؛ باقی price_source مظنهٔ زنده است */
/** فقط این منابع «آخرین قیمت ذخیره‌شده» هستند؛ باقی price_source قیمت زنده است */
const STORED_PRICE_SOURCES = ['database_last']
function hasShowableHomePrice(p = lastPrice.value): boolean {
@@ -319,7 +320,7 @@ function hasLiveQuote(p = lastPrice.value): boolean {
/**
* وضعیت نمایش قیمت در صفحه اصلی — همان منطق بک‌اند:
* خارج از ساعت کاری (close_price روشن) → show_home_price_when_closed روشن: آخرین قیمت؛ خاموش: closed
* داخل ساعت کاری → مظنه زنده: open ؛ وگرنه فقط با show_last_price آخرین قیمت؛ وگرنه no_quote
* داخل ساعت کاری → قیمت زنده: open ؛ وگرنه فقط با show_last_price آخرین قیمت؛ وگرنه no_quote
*/
export function getPriceDisplayState(): PriceDisplayState {
void productTradingHoursVersion.value
@@ -348,7 +349,7 @@ export function getPriceDisplayState(): PriceDisplayState {
/**
* اسکلتون تا وقتی قیمت قابل‌نمایش برسد، یا وضعیت بسته قطعی شود،
* یا بعد از اولین پاسخ خالی پنجرهٔ settle تمام شود (مظنه واقعاً نیست).
* یا بعد از اولین پاسخ خالی پنجرهٔ settle تمام شود (قیمت واقعاً نیست).
*/
export function isHomeQuotePending(): boolean {
const settings = useSettingsStore()