diff --git a/CHANGELOG_MINIAPP.md b/CHANGELOG_MINIAPP.md index 3b45486..e5fdd2b 100644 --- a/CHANGELOG_MINIAPP.md +++ b/CHANGELOG_MINIAPP.md @@ -1,5 +1,11 @@ # Доработки мини-приложения Clientright (TG/MAX и веб) +## Системные баннеры на экране приветствия (2026-02) +- **Баннер «Профиль не заполнен»** вынесен в отдельную зону справа от текста «Теперь ты в системе — можно продолжать» (на десктопе — колонка ~260px), чтобы не занимал полстраницы и не сдвигал контент. +- Реализовано **единое место для системных баннеров**: массив `systemBanners`, при одном баннере показывается один Alert, при нескольких — карусель (Ant Design Carousel). В будущем сюда можно добавлять другие критические уведомления. +- **Мобильная вёрстка**: баннер на всю ширину, нормальный перенос текста (без разбиения по слогам), кнопка «Заполнить профиль» переносится под текст, крестик закрытия остаётся в первой строке справа (через `order` и `flex-wrap`). +- **Профиль**: убрана дублирующая ссылка «Домой» из шапки карточки профиля — навигация остаётся через нижний бар. + ## UI и навигация - **«Мои обращения»**: дашборд с плитками по статусам (На рассмотрении, В работе, Решённые, Отклонённые, Все), заголовок переименован с «Жалобы потребителей». - Убрана внешняя рамка у дашборда; карточки с hover-эффектом (подъём, тень), единая высота плиток, прозрачный фон у иконок. diff --git a/frontend/src/pages/HelloAuth.css b/frontend/src/pages/HelloAuth.css index 982922b..5abf10d 100644 --- a/frontend/src/pages/HelloAuth.css +++ b/frontend/src/pages/HelloAuth.css @@ -52,10 +52,10 @@ } .hello-hero-body { - padding-top: 8px; - min-height: 140px; + padding: 8px 20px 16px; + min-height: 120px; display: flex; - align-items: center; + align-items: flex-start; justify-content: center; } @@ -64,6 +64,56 @@ 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 { margin-top: 32px; } @@ -204,6 +254,32 @@ --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) { padding: 16px 12px; gap: 8px; diff --git a/frontend/src/pages/HelloAuth.tsx b/frontend/src/pages/HelloAuth.tsx index e80f067..ea4a0e4 100644 --- a/frontend/src/pages/HelloAuth.tsx +++ b/frontend/src/pages/HelloAuth.tsx @@ -1,5 +1,5 @@ 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 { User, IdCard, @@ -22,6 +22,7 @@ type Status = 'idle' | 'loading' | 'success' | 'error'; interface HelloAuthProps { onAvatarChange?: (url: string) => void; onNavigate?: (path: string) => void; + onProfileNeedsAttentionChange?: (value: boolean) => void; } 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_TICK_MS = 250; -export default function HelloAuth({ onAvatarChange, onNavigate }: HelloAuthProps) { +export default function HelloAuth({ onAvatarChange, onNavigate, onProfileNeedsAttentionChange }: HelloAuthProps) { const [status, setStatus] = useState('idle'); const [greeting, setGreeting] = useState('Привет!'); const [error, setError] = useState(''); const [avatar, setAvatar] = useState(''); + const [profileNeedsAttention, setProfileNeedsAttention] = useState(false); + const [profileBannerDismissed, setProfileBannerDismissed] = useState(() => { + try { + return sessionStorage.getItem('profile_attention_banner_dismissed_v1') === '1'; + } catch (_) { + return false; + } + }); const [phone, setPhone] = useState(''); const [code, setCode] = useState(''); const [codeSent, setCodeSent] = useState(false); @@ -97,6 +106,15 @@ export default function HelloAuth({ onAvatarChange, onNavigate }: HelloAuthProps return 'need_contact'; } 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; if (token) { try { @@ -171,6 +189,15 @@ export default function HelloAuth({ onAvatarChange, onNavigate }: HelloAuthProps }); const verifyData = await verifyRes.json().catch(() => ({})); 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('Привет!'); const tgUser = (window as any).Telegram?.WebApp?.initDataUnsafe?.user; const maxUser = (window as any).WebApp?.initDataUnsafe?.user; @@ -260,7 +287,7 @@ export default function HelloAuth({ onAvatarChange, onNavigate }: HelloAuthProps }; tryAuth(); - }, [onAvatarChange, onNavigate]); + }, [onAvatarChange, onNavigate, onProfileNeedsAttentionChange]); if (noInitDataAfterTimeout && status === 'idle') { 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: ( + { + try { + sessionStorage.setItem('profile_attention_banner_dismissed_v1', '1'); + } catch (_) {} + setProfileBannerDismissed(true); + }} + action={ + + } + /> + ), + }); + } + + const hasSystemBanners = systemBanners.length > 0; + const tiles: Array<{ title: string; icon: typeof User; color: string; href?: string }> = [ { title: 'Мои обращения', icon: ClipboardList, color: '#6366F1', href: '/' }, { title: 'Подать жалобу', icon: FileWarning, color: '#EA580C', href: '/new' }, @@ -359,7 +423,31 @@ if (data.avatar_url) { {status === 'loading' ? ( ) : status === 'success' ? ( -

Теперь ты в системе — можно продолжать

+
+
+

Теперь ты в системе — можно продолжать

+
+ {hasSystemBanners && ( +
+ {systemBanners.length === 1 ? ( + systemBanners[0].node + ) : ( + + {systemBanners.map((banner) => ( +
+ {banner.node} +
+ ))} +
+ )} +
+ )} +
) : status === 'error' ? (

{error}

) : ( diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 33798be..5a19deb 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -207,7 +207,6 @@ export default function Profile({ onNavigate }: ProfileProps) { Профиль} - extra={onNavigate ? : null} >
{PROFILE_FIELDS.map(({ key, keys, label, editable }) => (