Port modular features into boursecu and fix desktop login UX.

Add support chat, piggy funds, PWA banner, notification/SSE sync, and messenger legacy guards while keeping boursecu theme; also balance login panels and unblock login navigation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-06 20:14:46 +02:00
co-authored by Cursor
parent be896fe247
commit 42aef88064
55 changed files with 7488 additions and 434 deletions
@@ -33,6 +33,20 @@
</span>
</span>
</button>
<button
v-if="chatEnabled"
type="button"
class="mhome-header-btn mhome-header-btn--notify"
aria-label="چت پشتیبانی"
@click.stop="goSupportChat"
>
<span class="mhome-header-btn__icon-wrap">
<Icon icon="vuesax-linear:messages-2" size="18" />
<span v-if="chatUnreadCount > 0" class="mhome-header-btn__badge">
{{ chatUnreadCount > 99 ? '99+' : chatUnreadCount }}
</span>
</span>
</button>
<button
type="button"
class="mhome-header-btn"
@@ -67,6 +81,7 @@ import {
hasNativeInstallPrompt,
initPwaInstallPromptListener,
} from '@/modules/pwa/pwaInstall'
import { unreadSupportCount, isUserSupportChatEnabled } from '@/utils/chat'
const headerRef = ref<HTMLElement | null>(null)
const isHeaderScrolled = ref(false)
@@ -74,6 +89,8 @@ const canNativeInstall = ref(false)
const route = useRoute()
const projectLogo = computed(() => getProjectMobileNavLogo())
const chatEnabled = computed(() => isUserSupportChatEnabled())
const chatUnreadCount = computed(() => unreadSupportCount.value)
const {
enabled: notificationEnabled,
unreadCount,
@@ -98,6 +115,10 @@ async function goNotifications() {
void router.push({ name: 'appNotifications' })
}
function goSupportChat() {
void router.push({ name: 'support' })
}
let intersectionObserver: IntersectionObserver | null = null
let scrollTarget: HTMLElement | Window | null = null
+31 -8
View File
@@ -39,10 +39,10 @@
<a-radio-button value="WEEKLY" @click="handleUpdate('WEEKLY')">هفته گذشته</a-radio-button>
<a-radio-button value="MONTHLY" @click="handleUpdate('MONTHLY')">ماه گذشته</a-radio-button>
</a-radio-group>
<div ref="chartContainerRef" id="chart-timeline" class="w-full" :class="chartInnerClass">
<div id="chart-timeline" class="w-full" :class="chartInnerClass">
<apexchart
v-if="canRenderChart"
:key="`${layout}-${variant}-${selectedMetal}-${chartSelection}-${seriesLength}`"
:key="chartRenderKey"
type="area"
width="100%"
:height="chartHeight"
@@ -66,7 +66,6 @@ import {
shouldUseKatodVisualChartTransform,
} from '@/utils/chartPriceAxis'
import { useSettingsStore } from '@/store/settingsStore'
import { useElementSize } from '@vueuse/core'
import { price } from '@/utils/price'
import type { ChartMetalType } from '../composables/usePriceChart'
@@ -117,12 +116,10 @@ const emit = defineEmits<{
const config = useConfigStore()
const settingsStore = useSettingsStore()
const { selectedMetalChartRatio } = usePriceChart()
const chartContainerRef = ref<HTMLElement | null>(null)
const chartMounted = ref(false)
const renderNonce = ref(0)
const selectedValue = ref(props.chartSelection)
const { width: chartWidth } = useElementSize(chartContainerRef)
const seriesLength = computed(() => props.priceChartSeries?.[0]?.data?.length ?? 0)
const amplifyKatodChart = computed(() =>
shouldAmplifyKatodMesChart({
@@ -175,20 +172,46 @@ const chartYBounds = computed(() => {
return computeKatodMesVisualYBounds(prices)
})
const canRenderChart = computed(
() => chartMounted.value && chartWidth.value > 0 && seriesLength.value > 0
/** Do not gate on measured width — useElementSize can stay 0 until a reflow (e.g. tab click). */
const canRenderChart = computed(() => chartMounted.value && seriesLength.value > 0)
const chartRenderKey = computed(
() =>
`${props.layout}-${props.variant}-${props.selectedMetal}-${props.chartSelection}-${seriesLength.value}-${renderNonce.value}`
)
function scheduleChartRemount() {
void nextTick(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
renderNonce.value += 1
})
})
})
}
watch(
() => props.chartSelection,
(timeline) => {
selectedValue.value = timeline
if (seriesLength.value > 0) {
scheduleChartRemount()
}
}
)
// Data often arrives before/while first layout; Apex can mount blank until remount.
watch(seriesLength, (len, prev) => {
if (len > 0 && (prev ?? 0) === 0) {
scheduleChartRemount()
}
})
onMounted(async () => {
await nextTick()
chartMounted.value = true
if (seriesLength.value > 0) {
scheduleChartRemount()
}
})
const chartOptions = computed(() => {