Banner: system banners zone carousel mobile layout remove Home from Profile
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Доработки мини-приложения Clientright (TG/MAX и веб)
|
# Доработки мини-приложения Clientright (TG/MAX и веб)
|
||||||
|
|
||||||
|
## Системные баннеры на экране приветствия (2026-02)
|
||||||
|
- **Баннер «Профиль не заполнен»** вынесен в отдельную зону справа от текста «Теперь ты в системе — можно продолжать» (на десктопе — колонка ~260px), чтобы не занимал полстраницы и не сдвигал контент.
|
||||||
|
- Реализовано **единое место для системных баннеров**: массив `systemBanners`, при одном баннере показывается один Alert, при нескольких — карусель (Ant Design Carousel). В будущем сюда можно добавлять другие критические уведомления.
|
||||||
|
- **Мобильная вёрстка**: баннер на всю ширину, нормальный перенос текста (без разбиения по слогам), кнопка «Заполнить профиль» переносится под текст, крестик закрытия остаётся в первой строке справа (через `order` и `flex-wrap`).
|
||||||
|
- **Профиль**: убрана дублирующая ссылка «Домой» из шапки карточки профиля — навигация остаётся через нижний бар.
|
||||||
|
|
||||||
## UI и навигация
|
## UI и навигация
|
||||||
- **«Мои обращения»**: дашборд с плитками по статусам (На рассмотрении, В работе, Решённые, Отклонённые, Все), заголовок переименован с «Жалобы потребителей».
|
- **«Мои обращения»**: дашборд с плитками по статусам (На рассмотрении, В работе, Решённые, Отклонённые, Все), заголовок переименован с «Жалобы потребителей».
|
||||||
- Убрана внешняя рамка у дашборда; карточки с hover-эффектом (подъём, тень), единая высота плиток, прозрачный фон у иконок.
|
- Убрана внешняя рамка у дашборда; карточки с hover-эффектом (подъём, тень), единая высота плиток, прозрачный фон у иконок.
|
||||||
|
|||||||
@@ -52,10 +52,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hello-hero-body {
|
.hello-hero-body {
|
||||||
padding-top: 8px;
|
padding: 8px 20px 16px;
|
||||||
min-height: 140px;
|
min-height: 120px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +64,56 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hello-hero-success {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello-hero-success-main {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello-hero-system {
|
||||||
|
flex: 0 0 260px;
|
||||||
|
max-width: 260px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello-hero-system :where(.ant-alert) {
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello-hero-system :where(.ant-alert-content) {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello-hero-system :where(.ant-alert-message) {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello-hero-system :where(.ant-alert-description) {
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello-hero-system-carousel .hello-hero-system-slide {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.hello-grid {
|
.hello-grid {
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
}
|
}
|
||||||
@@ -204,6 +254,32 @@
|
|||||||
--tile-h: 140px;
|
--tile-h: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hello-hero-success {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello-hero-system {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* На узких экранах баннер на всю ширину, кнопка под текстом — текст не сжимается */
|
||||||
|
.hello-hero-system :where(.ant-alert) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.hello-hero-system :where(.ant-alert-action) {
|
||||||
|
flex-basis: 100%;
|
||||||
|
order: 1;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.hello-hero-system :where(.ant-alert-close-icon) {
|
||||||
|
order: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.tile-card :where(.ant-card-body) {
|
.tile-card :where(.ant-card-body) {
|
||||||
padding: 16px 12px;
|
padding: 16px 12px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Card, Button, Input, Space, Spin, message, Row, Col } from 'antd';
|
import { Card, Button, Input, Space, Spin, message, Row, Col, Alert, Carousel } from 'antd';
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
IdCard,
|
IdCard,
|
||||||
@@ -22,6 +22,7 @@ type Status = 'idle' | 'loading' | 'success' | 'error';
|
|||||||
interface HelloAuthProps {
|
interface HelloAuthProps {
|
||||||
onAvatarChange?: (url: string) => void;
|
onAvatarChange?: (url: string) => void;
|
||||||
onNavigate?: (path: string) => void;
|
onNavigate?: (path: string) => void;
|
||||||
|
onProfileNeedsAttentionChange?: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INIT_DATA_WAIT_MS = 5500;
|
const INIT_DATA_WAIT_MS = 5500;
|
||||||
@@ -29,11 +30,19 @@ const INIT_DATA_POLL_MS = 200;
|
|||||||
const INIT_DATA_BG_RECOVERY_MS = 15000;
|
const INIT_DATA_BG_RECOVERY_MS = 15000;
|
||||||
const INIT_DATA_BG_TICK_MS = 250;
|
const INIT_DATA_BG_TICK_MS = 250;
|
||||||
|
|
||||||
export default function HelloAuth({ onAvatarChange, onNavigate }: HelloAuthProps) {
|
export default function HelloAuth({ onAvatarChange, onNavigate, onProfileNeedsAttentionChange }: HelloAuthProps) {
|
||||||
const [status, setStatus] = useState<Status>('idle');
|
const [status, setStatus] = useState<Status>('idle');
|
||||||
const [greeting, setGreeting] = useState<string>('Привет!');
|
const [greeting, setGreeting] = useState<string>('Привет!');
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
const [avatar, setAvatar] = useState<string>('');
|
const [avatar, setAvatar] = useState<string>('');
|
||||||
|
const [profileNeedsAttention, setProfileNeedsAttention] = useState<boolean>(false);
|
||||||
|
const [profileBannerDismissed, setProfileBannerDismissed] = useState<boolean>(() => {
|
||||||
|
try {
|
||||||
|
return sessionStorage.getItem('profile_attention_banner_dismissed_v1') === '1';
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
const [phone, setPhone] = useState<string>('');
|
const [phone, setPhone] = useState<string>('');
|
||||||
const [code, setCode] = useState<string>('');
|
const [code, setCode] = useState<string>('');
|
||||||
const [codeSent, setCodeSent] = useState<boolean>(false);
|
const [codeSent, setCodeSent] = useState<boolean>(false);
|
||||||
@@ -97,6 +106,15 @@ export default function HelloAuth({ onAvatarChange, onNavigate }: HelloAuthProps
|
|||||||
return 'need_contact';
|
return 'need_contact';
|
||||||
}
|
}
|
||||||
if (res.ok && data.success) {
|
if (res.ok && data.success) {
|
||||||
|
const needsAttentionRaw =
|
||||||
|
(data.profile_needs_attention as unknown) ??
|
||||||
|
(data.profileNeedsAttention as unknown) ??
|
||||||
|
(data.need_profile_confirm as unknown) ??
|
||||||
|
(data.needProfileConfirm as unknown);
|
||||||
|
const needsAttention = needsAttentionRaw === true || needsAttentionRaw === 1 || needsAttentionRaw === '1' || needsAttentionRaw === 'true';
|
||||||
|
setProfileNeedsAttention(needsAttention);
|
||||||
|
onProfileNeedsAttentionChange?.(needsAttention);
|
||||||
|
|
||||||
const token = data.session_token as string | undefined;
|
const token = data.session_token as string | undefined;
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
@@ -171,6 +189,15 @@ export default function HelloAuth({ onAvatarChange, onNavigate }: HelloAuthProps
|
|||||||
});
|
});
|
||||||
const verifyData = await verifyRes.json().catch(() => ({}));
|
const verifyData = await verifyRes.json().catch(() => ({}));
|
||||||
if (verifyData?.valid === true) {
|
if (verifyData?.valid === true) {
|
||||||
|
const needsAttentionRaw =
|
||||||
|
verifyData.profile_needs_attention ??
|
||||||
|
verifyData.profileNeedsAttention ??
|
||||||
|
verifyData.need_profile_confirm ??
|
||||||
|
verifyData.needProfileConfirm;
|
||||||
|
const needsAttention = needsAttentionRaw === true || needsAttentionRaw === 1 || needsAttentionRaw === '1' || needsAttentionRaw === 'true';
|
||||||
|
setProfileNeedsAttention(needsAttention);
|
||||||
|
onProfileNeedsAttentionChange?.(needsAttention);
|
||||||
|
|
||||||
setGreeting('Привет!');
|
setGreeting('Привет!');
|
||||||
const tgUser = (window as any).Telegram?.WebApp?.initDataUnsafe?.user;
|
const tgUser = (window as any).Telegram?.WebApp?.initDataUnsafe?.user;
|
||||||
const maxUser = (window as any).WebApp?.initDataUnsafe?.user;
|
const maxUser = (window as any).WebApp?.initDataUnsafe?.user;
|
||||||
@@ -260,7 +287,7 @@ export default function HelloAuth({ onAvatarChange, onNavigate }: HelloAuthProps
|
|||||||
};
|
};
|
||||||
|
|
||||||
tryAuth();
|
tryAuth();
|
||||||
}, [onAvatarChange, onNavigate]);
|
}, [onAvatarChange, onNavigate, onProfileNeedsAttentionChange]);
|
||||||
|
|
||||||
if (noInitDataAfterTimeout && status === 'idle') {
|
if (noInitDataAfterTimeout && status === 'idle') {
|
||||||
return (
|
return (
|
||||||
@@ -325,6 +352,43 @@ if (data.avatar_url) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const systemBanners: Array<{ key: string; node: JSX.Element }> = [];
|
||||||
|
|
||||||
|
if (status === 'success' && profileNeedsAttention && !profileBannerDismissed) {
|
||||||
|
systemBanners.push({
|
||||||
|
key: 'profile-not-complete',
|
||||||
|
node: (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
closable
|
||||||
|
message="Профиль не заполнен"
|
||||||
|
description="Пожалуйста, заполните обязательные поля профиля, чтобы продолжить работу."
|
||||||
|
onClose={() => {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem('profile_attention_banner_dismissed_v1', '1');
|
||||||
|
} catch (_) {}
|
||||||
|
setProfileBannerDismissed(true);
|
||||||
|
}}
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (onNavigate) onNavigate('/profile');
|
||||||
|
else window.location.href = '/profile';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Заполнить профиль
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSystemBanners = systemBanners.length > 0;
|
||||||
|
|
||||||
const tiles: Array<{ title: string; icon: typeof User; color: string; href?: string }> = [
|
const tiles: Array<{ title: string; icon: typeof User; color: string; href?: string }> = [
|
||||||
{ title: 'Мои обращения', icon: ClipboardList, color: '#6366F1', href: '/' },
|
{ title: 'Мои обращения', icon: ClipboardList, color: '#6366F1', href: '/' },
|
||||||
{ title: 'Подать жалобу', icon: FileWarning, color: '#EA580C', href: '/new' },
|
{ title: 'Подать жалобу', icon: FileWarning, color: '#EA580C', href: '/new' },
|
||||||
@@ -359,7 +423,31 @@ if (data.avatar_url) {
|
|||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
<Spin size="large" tip="Авторизация..." />
|
<Spin size="large" tip="Авторизация..." />
|
||||||
) : status === 'success' ? (
|
) : status === 'success' ? (
|
||||||
<p>Теперь ты в системе — можно продолжать</p>
|
<div className={`hello-hero-success${hasSystemBanners ? ' hello-hero-success--with-banner' : ''}`}>
|
||||||
|
<div className="hello-hero-success-main">
|
||||||
|
<p>Теперь ты в системе — можно продолжать</p>
|
||||||
|
</div>
|
||||||
|
{hasSystemBanners && (
|
||||||
|
<div className="hello-hero-system">
|
||||||
|
{systemBanners.length === 1 ? (
|
||||||
|
systemBanners[0].node
|
||||||
|
) : (
|
||||||
|
<Carousel
|
||||||
|
autoplay
|
||||||
|
dots
|
||||||
|
adaptiveHeight
|
||||||
|
className="hello-hero-system-carousel"
|
||||||
|
>
|
||||||
|
{systemBanners.map((banner) => (
|
||||||
|
<div key={banner.key} className="hello-hero-system-slide">
|
||||||
|
{banner.node}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : status === 'error' ? (
|
) : status === 'error' ? (
|
||||||
<p className="hello-hero-error">{error}</p>
|
<p className="hello-hero-error">{error}</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -207,7 +207,6 @@ export default function Profile({ onNavigate }: ProfileProps) {
|
|||||||
<Card
|
<Card
|
||||||
className="profile-card"
|
className="profile-card"
|
||||||
title={<><User size={20} style={{ marginRight: 8, verticalAlign: 'middle' }} /> Профиль</>}
|
title={<><User size={20} style={{ marginRight: 8, verticalAlign: 'middle' }} /> Профиль</>}
|
||||||
extra={onNavigate ? <Button type="link" onClick={() => onNavigate('/')}>Домой</Button> : null}
|
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" onFinish={handleSave}>
|
<Form form={form} layout="vertical" onFinish={handleSave}>
|
||||||
{PROFILE_FIELDS.map(({ key, keys, label, editable }) => (
|
{PROFILE_FIELDS.map(({ key, keys, label, editable }) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user