Componentize dashboard middle section and add Mento brand assets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 16:05:29 +03:30
co-authored by Cursor
parent da6c494fcb
commit 57fa6c8d12
15 changed files with 1491 additions and 1015 deletions
@@ -0,0 +1,170 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { HomeDashboardTrendPointDto } from '@mento/shared';
const props = defineProps<{
trend: HomeDashboardTrendPointDto[];
changePercent: number | null;
formatChange: (value: number | null) => string;
formatAmount: (value: number) => string;
}>();
const points = computed(() => {
const trend = props.trend;
if (!trend.length) return [];
const max = Math.max(...trend.map((p) => p.amount), 1);
const w = 360;
const h = 150;
const padX = 16;
const padY = 18;
return trend.map((p, i) => ({
...p,
x: padX + (i / Math.max(trend.length - 1, 1)) * (w - padX * 2),
y: h - padY - (p.amount / max) * (h - padY * 2),
}));
});
const line = computed(() => points.value.map((p) => `${p.x},${p.y}`).join(' '));
const area = computed(() => {
if (!points.value.length) return '';
const first = points.value[0]!;
const last = points.value[points.value.length - 1]!;
return `${line.value} ${last.x},160 ${first.x},160`;
});
const highlight = computed(() => {
if (!points.value.length) return null;
return points.value.reduce((best, cur) => (cur.amount >= best.amount ? cur : best));
});
</script>
<template>
<article class="panel">
<header class="panel-head">
<h2>روند فروش</h2>
<span
v-if="changePercent != null"
class="mini-badge"
:class="{ down: changePercent < 0 }"
>
{{ formatChange(changePercent) }} نسبت به ماه قبل
</span>
</header>
<div class="chart-wrap">
<svg viewBox="0 0 360 170" class="line-chart" aria-hidden="true">
<defs>
<linearGradient id="homeTrendFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#14b8a6" stop-opacity="0.3" />
<stop offset="100%" stop-color="#14b8a6" stop-opacity="0" />
</linearGradient>
</defs>
<line
v-for="y in [40, 75, 110, 145]"
:key="y"
x1="12"
:y1="y"
x2="348"
:y2="y"
stroke="#eef2f7"
stroke-width="1"
/>
<polygon v-if="area" :points="area" fill="url(#homeTrendFill)" />
<polyline
v-if="line"
:points="line"
fill="none"
stroke="#14b8a6"
stroke-width="3.4"
stroke-linecap="round"
stroke-linejoin="round"
/>
<g v-for="(p, i) in points" :key="i">
<circle :cx="p.x" :cy="p.y" r="5" fill="#fff" stroke="#14b8a6" stroke-width="2.6" />
</g>
<g v-if="highlight">
<rect
:x="highlight.x - 52"
:y="highlight.y - 36"
width="104"
height="26"
rx="10"
fill="#0f766e"
/>
<text
:x="highlight.x"
:y="highlight.y - 18"
text-anchor="middle"
fill="#fff"
font-size="10"
font-family="Vazirmatn, Tahoma, sans-serif"
>
{{ formatAmount(highlight.amount) }}
</text>
</g>
</svg>
<div class="chart-labels">
<span v-for="p in trend" :key="p.monthKey">{{ p.monthLabel }}</span>
</div>
</div>
</article>
</template>
<style scoped>
.panel {
background: #fff;
border-radius: 22px;
padding: 1.05rem 1.15rem;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
border: 1px solid #eef2f7;
min-width: 0;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.65rem;
}
h2 {
margin: 0;
font-size: 0.98rem;
color: #0f172a;
}
.mini-badge {
font-size: 0.72rem;
font-weight: 700;
color: #16a34a;
background: #dcfce7;
border-radius: 999px;
padding: 0.2rem 0.55rem;
white-space: nowrap;
}
.mini-badge.down {
color: #dc2626;
background: #fee2e2;
}
.chart-wrap {
display: grid;
gap: 0.2rem;
}
.line-chart {
width: 100%;
height: auto;
}
.chart-labels {
display: flex;
justify-content: space-between;
color: #94a3b8;
font-size: 0.74rem;
padding: 0 0.4rem;
}
</style>