Files
CRM_Montzar/apps/web/src/components/dashboard/HomeHero.vue
T

462 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed } from 'vue';
import { RouterLink } from 'vue-router';
import UiIcon from '@/components/UiIcon.vue';
import MentoAiAvatar from '@/components/brand/MentoAiAvatar.vue';
import { brand, type HomeDashboardAlertDto } from '@mento/shared';
import { useAuthStore } from '@/stores/auth';
const props = defineProps<{
greeting: string;
firstName: string;
userInitial: string;
alerts: HomeDashboardAlertDto[];
askQuery: string;
asking?: boolean;
askAnswer?: string;
askError?: string;
statusText?: string;
}>();
const emit = defineEmits<{
'update:askQuery': [value: string];
ask: [];
}>();
const auth = useAuthStore();
type HeroChip = {
key: string;
tone: HomeDashboardAlertDto['tone'];
title: string;
subtitle?: string;
link: string;
};
const chips = computed<HeroChip[]>(() =>
props.alerts.slice(0, 3).map((a) => {
const parts = a.text.split(/\s*[—\-–]\s*|\n/);
const title = parts[0]?.trim() || a.text;
const subtitle = parts.slice(1).join(' ').trim() || undefined;
return {
key: a.key,
tone: a.tone,
title,
subtitle,
link: a.link || '/',
};
}),
);
function alertIcon(tone: string) {
if (tone === 'urgent') return 'fire' as const;
if (tone === 'warning') return 'warning' as const;
if (tone === 'success') return 'trend' as const;
return 'sparkle' as const;
}
const shortcutLabel = computed(() =>
typeof navigator !== 'undefined' && /Mac|iPhone|iPad/i.test(navigator.platform)
? '⌘K'
: 'Ctrl+K',
);
</script>
<template>
<header class="hero">
<div class="hero-surface" aria-hidden="true">
<div class="hero-glow" />
<div class="hero-mist" />
</div>
<div class="hero-body">
<div class="hero-greet">
<span class="hero-avatar">
<img :src="auth.avatarSrc" :alt="firstName" width="72" height="72" />
<i class="online" />
</span>
<div class="greet-text">
<h1>{{ greeting }} {{ firstName }} 👋</h1>
<p v-if="chips.length">{{ brand.nameFa }} اینا رو برات پیدا کرده</p>
<p v-else>از {{ brand.nameFa }} بپرس یا کار امروز را شروع کن</p>
</div>
</div>
<div v-if="chips.length" class="hero-chips">
<RouterLink
v-for="chip in chips"
:key="chip.key"
class="chip"
:class="`chip-${chip.tone}`"
:to="chip.link"
>
<span class="chip-ico"><UiIcon :name="alertIcon(chip.tone)" :size="13" /></span>
<span class="chip-text">
<strong>{{ chip.title }}</strong>
<small v-if="chip.subtitle">{{ chip.subtitle }}</small>
</span>
</RouterLink>
</div>
<div class="hero-ask-wrap">
<form class="hero-ask" @submit.prevent="emit('ask')">
<UiIcon name="sparkle" :size="18" class="ask-ico" />
<input
id="home-hero-ask"
:value="askQuery"
type="search"
:placeholder="brand.askPlaceholderFa"
:disabled="asking"
@input="emit('update:askQuery', ($event.target as HTMLInputElement).value)"
/>
<kbd>{{ shortcutLabel }}</kbd>
<button type="submit" class="ask-send" aria-label="ارسال" :disabled="asking">
<UiIcon name="send" :size="15" />
</button>
</form>
<p v-if="asking" class="hero-ask-status">{{ statusText || 'در حال پاسخ…' }}</p>
<p v-else-if="askError" class="hero-ask-status err">{{ askError }}</p>
<p v-else-if="askAnswer" class="hero-ask-status">{{ askAnswer }}</p>
</div>
</div>
<MentoAiAvatar class="hero-bot" :size="156" />
</header>
</template>
<style scoped>
.hero {
position: relative;
display: grid;
/* RTL: body سمت راست، ربات سمت چپ — فضای اضافه برای پاپ‌آپ */
grid-template-columns: minmax(0, 1fr) 210px;
gap: 0.7rem 1rem;
align-items: start;
min-height: 0;
padding: 0.8rem 1.5rem 0.9rem;
border-radius: var(--radius);
overflow: visible;
color: #fff;
background: transparent;
isolation: isolate;
}
.hero-surface {
position: absolute;
inset: 0;
z-index: 0;
border-radius: var(--radius);
overflow: hidden;
background-color: #134e4a;
background-image:
radial-gradient(ellipse 55% 90% at 8% 55%, rgba(103, 232, 249, 0.55) 0%, rgba(45, 212, 191, 0.22) 35%, transparent 68%),
radial-gradient(ellipse 50% 80% at 92% 40%, rgba(153, 246, 228, 0.28) 0%, transparent 55%),
radial-gradient(ellipse 70% 60% at 45% 30%, rgba(45, 212, 191, 0.18) 0%, transparent 60%),
radial-gradient(ellipse 60% 50% at 60% 85%, rgba(15, 23, 42, 0.28) 0%, transparent 55%),
linear-gradient(
105deg,
#0b3d3a 0%,
#115e59 22%,
#0f766e 48%,
#134e4a 72%,
#0f3f3c 100%
);
box-shadow:
0 16px 36px rgba(15, 118, 110, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.18),
inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.hero-glow {
position: absolute;
width: 320px;
height: 320px;
border-radius: 999px;
inset-inline-end: -70px;
top: 40%;
transform: translateY(-50%);
background:
radial-gradient(circle, rgba(165, 243, 252, 0.65) 0%, rgba(34, 211, 238, 0.28) 32%, transparent 68%);
filter: blur(8px);
pointer-events: none;
}
.hero-mist {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 40% 35% at 25% 20%, rgba(255, 255, 255, 0.14) 0%, transparent 70%),
radial-gradient(ellipse 35% 40% at 70% 75%, rgba(204, 251, 241, 0.12) 0%, transparent 70%),
radial-gradient(ellipse 50% 45% at 50% 50%, rgba(15, 118, 110, 0.35) 0%, transparent 75%);
pointer-events: none;
}
.hero-body,
.hero-bot {
position: relative;
z-index: 1;
}
.hero-body {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.75rem;
min-width: 0;
max-width: 100%;
padding-top: 0;
}
.hero-greet {
display: flex;
align-items: center;
gap: 0.9rem;
min-width: 0;
max-width: 100%;
}
.greet-text {
min-width: 0;
}
.hero-avatar {
position: relative;
width: 72px;
height: 72px;
border-radius: 999px;
flex-shrink: 0;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
}
.hero-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: 999px;
border: 3px solid rgba(255, 255, 255, 0.65);
background: rgba(255, 255, 255, 0.22);
box-sizing: border-box;
}
.online {
position: absolute;
bottom: 3px;
inset-inline-start: 3px;
width: 14px;
height: 14px;
border-radius: 999px;
background: #22c55e;
border: 2px solid #fff;
z-index: 1;
}
.greet-text h1 {
margin: 0;
font-size: clamp(1.35rem, 2vw, 1.75rem);
font-weight: 600;
letter-spacing: -0.01em;
color: #fff;
}
.greet-text p {
margin: 0.25rem 0 0;
color: rgba(255, 255, 255, 0.88);
font-size: 0.9rem;
font-weight: 500;
}
.hero-chips {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.4rem;
max-width: min(580px, 100%);
min-width: 0;
}
.chip {
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
padding: 0.4rem 0.5rem;
border-radius: var(--radius);
background: #fff;
color: #0f172a;
text-decoration: none;
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.07);
}
.chip-text {
display: grid;
gap: 0.04rem;
min-width: 0;
}
.chip-text strong {
font-size: 0.68rem;
font-weight: 700;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chip-text small {
font-size: 0.6rem;
color: #64748b;
font-weight: 500;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chip-ico {
width: 22px;
height: 22px;
border-radius: var(--radius);
display: grid;
place-items: center;
flex-shrink: 0;
}
.chip-urgent .chip-ico {
background: #fee2e2;
color: #ef4444;
}
.chip-warning .chip-ico {
background: #fef3c7;
color: #d97706;
}
.chip-success .chip-ico,
.chip-info .chip-ico {
background: #dcfce7;
color: #16a34a;
}
.hero-ask-wrap {
display: grid;
gap: 0.45rem;
max-width: min(520px, 100%);
width: 100%;
min-width: 0;
}
.hero-ask {
display: flex;
align-items: center;
gap: 0.55rem;
width: 100%;
min-width: 0;
max-width: 100%;
background: #fff;
color: #0f172a;
border-radius: 999px;
padding: 0.45rem 0.45rem 0.45rem 0.9rem;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.1);
}
.ask-ico {
color: #14b8a6;
flex-shrink: 0;
}
.hero-ask input {
border: 0;
background: transparent;
flex: 1;
outline: none;
font-size: 0.9rem;
min-width: 0;
}
.hero-ask input::placeholder {
color: #94a3b8;
}
.hero-ask kbd {
font-size: 0.68rem;
color: #94a3b8;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: var(--radius);
padding: 0.14rem 0.38rem;
flex-shrink: 0;
}
.ask-send {
width: 34px;
height: 34px;
border: 0;
border-radius: 999px;
background: #14b8a6;
color: #fff;
cursor: pointer;
display: grid;
place-items: center;
flex-shrink: 0;
}
.ask-send:disabled {
opacity: 0.55;
cursor: wait;
}
.hero-ask-status {
margin: 0;
padding: 0.55rem 0.85rem;
border-radius: 14px;
background: rgba(255, 255, 255, 0.16);
color: #fff;
font-size: 0.82rem;
line-height: 1.7;
max-height: 7.5rem;
overflow: auto;
}
.hero-ask-status.err {
background: rgba(185, 28, 28, 0.28);
}
.hero-bot {
position: relative;
z-index: 1;
/* در RTL ستون ربات سمت چپ است؛ end = لبه بیرونی چپ */
justify-self: end;
align-self: center;
margin-inline-start: 0;
margin-inline-end: 0.35rem;
overflow: visible;
}
@media (max-width: 900px) {
.hero {
grid-template-columns: minmax(0, 1fr) 160px;
}
.hero-chips {
grid-template-columns: minmax(0, 1fr);
max-width: min(420px, 100%);
}
}
@media (max-width: 700px) {
.hero {
grid-template-columns: minmax(0, 1fr);
min-height: auto;
padding-inline: 0.85rem;
overflow: hidden;
}
.hero-bot {
display: none;
}
.hero-ask kbd {
display: none;
}
}
</style>