Добавлены изменения для PWA

This commit is contained in:
mi
2026-07-21 18:49:21 +03:00
parent 627371b221
commit cc0163eb94
22 changed files with 592 additions and 208 deletions
Binary file not shown.
+6 -6
View File
@@ -1,13 +1,11 @@
export function HanLogo() {
return (
<div className="flex items-center gap-3 px-4 py-3">
<div className="flex items-center justify-center w-12 h-12 rounded-2xl bg-primary text-primary-foreground flex-shrink-0">
<svg
viewBox="0 0 100 100"
className="w-7 h-7"
fill="currentColor"
>
<path d="M20 25 L20 75 M20 50 L50 50 M50 25 L50 75 M70 25 L90 25 L90 50 L70 50 L70 75 L90 75"
<svg viewBox="0 0 100 100" className="w-7 h-7" fill="currentColor">
<path
d="M20 25 L20 75 M20 50 L50 50 M50 25 L50 75 M70 25 L90 25 L90 50 L70 50 L70 75 L90 75"
stroke="currentColor"
strokeWidth="6"
fill="none"
@@ -16,12 +14,14 @@ export function HanLogo() {
/>
</svg>
</div>
<div className="flex-1">
<h1 className="text-base font-medium mb-0.5">Привет! Я HAN</h1>
<p className="text-xs text-muted-foreground">
Помощник по документам и жизни в России
</p>
</div>
</div>
);
}
+14 -5
View File
@@ -1,17 +1,26 @@
import { User, History } from 'lucide-react';
import { User, Bell } from 'lucide-react';
import { Link } from 'react-router';
import { getActiveMessages } from '../data/companyMessages';
export function Header() {
const count = getActiveMessages().length;
return (
<header className="flex items-center justify-between px-4 py-3 bg-background border-b border-border">
<Link to="/history" className="flex items-center gap-2 text-foreground hover:opacity-70 transition-opacity">
<History className="w-5 h-5" />
<span className="text-sm">История</span>
{/* Колокольчик с бейджем */}
<Link to="/history" className="relative flex items-center justify-center w-10 h-10 rounded-full hover:bg-muted transition-colors">
<Bell className="w-5 h-5" />
{count > 0 && (
<span className="absolute top-0.5 right-0.5 min-w-[16px] h-4 px-1 flex items-center justify-center bg-destructive text-white text-[10px] font-bold rounded-full leading-none">
{count}
</span>
)}
</Link>
{/* Профиль */}
<Link to="/profile" className="relative flex items-center justify-center w-10 h-10 rounded-full bg-muted hover:opacity-70 transition-opacity">
<User className="w-5 h-5 text-muted-foreground" />
<span className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-primary rounded-full border-2 border-background"></span>
<span className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-primary rounded-full border-2 border-background" />
</Link>
</header>
);
+167 -43
View File
@@ -1,61 +1,185 @@
import { Calendar, FileText } from 'lucide-react';
import { useState, useCallback } from 'react';
import { useNavigate } from 'react-router';
import {
Calendar, AlertCircle, Bell, MessageCircle, Tag,
X, ChevronLeft, ChevronRight, ArrowRight,
} from 'lucide-react';
import { getActiveMessages, dismissMessage, CompanyMessage } from '../data/companyMessages';
interface Notification {
id: string;
type: 'urgent' | 'reminder';
// ─── Тип-конфиг ──────────────────────────────────────────────────────────────
type TypeConfig = {
label: string;
icon: React.ReactNode;
title: string;
description: string;
action: string;
bg: string;
iconWrap: string;
badge: string;
actionColor: string;
};
function getConfig(type: CompanyMessage['type']): TypeConfig {
switch (type) {
case 'urgent':
return {
label: 'Срочно',
icon: <AlertCircle className="w-4 h-4" />,
bg: 'bg-destructive/8 border-destructive/25',
iconWrap: 'bg-destructive/12 text-destructive',
badge: 'bg-destructive/12 text-destructive',
actionColor: 'text-destructive',
};
case 'reminder':
return {
label: 'Напоминание',
icon: <Calendar className="w-4 h-4" />,
bg: 'bg-primary/6 border-primary/20',
iconWrap: 'bg-primary/10 text-primary',
badge: 'bg-primary/10 text-primary',
actionColor: 'text-primary',
};
case 'message':
return {
label: 'Сообщение',
icon: <MessageCircle className="w-4 h-4" />,
bg: 'bg-[#e8f5e9] border-[#a5d6a7]',
iconWrap: 'bg-[#c8e6c9] text-[#2e7d32]',
badge: 'bg-[#c8e6c9] text-[#2e7d32]',
actionColor: 'text-[#2e7d32]',
};
case 'promo':
return {
label: 'Предложение',
icon: <Tag className="w-4 h-4" />,
bg: 'bg-[#fff8e1] border-[#ffe082]',
iconWrap: 'bg-[#fff3cd] text-[#e65100]',
badge: 'bg-[#fff3cd] text-[#e65100]',
actionColor: 'text-[#e65100]',
};
default:
return {
label: 'Новость',
icon: <Bell className="w-4 h-4" />,
bg: 'bg-muted/60 border-border',
iconWrap: 'bg-muted text-muted-foreground',
badge: 'bg-muted text-muted-foreground',
actionColor: 'text-foreground/70',
};
}
}
const notifications: Notification[] = [
{
id: '1',
type: 'urgent',
icon: <Calendar className="w-4 h-4" />,
title: 'Продление патента через 14 дней',
description: 'Не забудьте подать документы заранее',
action: 'Подробнее'
},
{
id: '2',
type: 'reminder',
icon: <FileText className="w-4 h-4" />,
title: 'Проверьте статус РВП',
description: 'Возможно, уже готово к получению',
action: 'Проверить'
}
];
// ─── Основной компонент ───────────────────────────────────────────────────────
export function Notifications() {
const navigate = useNavigate();
const [messages, setMessages] = useState(() => getActiveMessages());
const [index, setIndex] = useState(0);
if (notifications.length === 0) return null;
const handleDismiss = useCallback((id: string) => {
dismissMessage(id);
const next = getActiveMessages();
setMessages(next);
setIndex(i => Math.min(i, Math.max(next.length - 1, 0)));
}, []);
if (messages.length === 0) return null;
const msg = messages[index];
const cfg = getConfig(msg.type);
const hasMultiple = messages.length > 1;
function handleAction() {
if (msg.type === 'message' && msg.chatId) {
navigate(`/chat/${msg.chatId}`);
} else if (msg.type === 'promo') {
// внешний переход — в реальном приложении будет ссылка
navigate(`/notification/${msg.id}`);
} else {
navigate(`/notification/${msg.id}`);
}
}
const actionLabel =
msg.type === 'message' ? 'Открыть чат →' :
msg.type === 'promo' ? (msg.promo?.cta ?? 'Подробнее') + ' →' :
'Подробнее →';
return (
<div className="px-4 py-3 space-y-2">
{notifications.map((notification) => (
<div
key={notification.id}
className="bg-primary/5 border border-primary/20 rounded-lg p-3 flex items-start gap-2.5"
>
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary flex-shrink-0 mt-0.5">
{notification.icon}
<div className="px-4 py-3">
<div className={`rounded-xl border ${cfg.bg} overflow-hidden`}>
{/* Верхняя строка: метка + навигация + закрыть */}
<div className="flex items-center justify-between px-3 pt-2.5 pb-2">
<span className={`text-[11px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full ${cfg.badge}`}>
{cfg.label}
</span>
<div className="flex items-center gap-0.5">
{hasMultiple && (
<>
<button
onClick={() => setIndex(i => (i - 1 + messages.length) % messages.length)}
className="w-6 h-6 flex items-center justify-center rounded-md hover:bg-black/8 transition-colors"
>
<ChevronLeft className="w-3.5 h-3.5 text-muted-foreground" />
</button>
<span className="text-[11px] text-muted-foreground tabular-nums min-w-[28px] text-center">
{index + 1}/{messages.length}
</span>
<button
onClick={() => setIndex(i => (i + 1) % messages.length)}
className="w-6 h-6 flex items-center justify-center rounded-md hover:bg-black/8 transition-colors"
>
<ChevronRight className="w-3.5 h-3.5 text-muted-foreground" />
</button>
</>
)}
<button
onClick={() => handleDismiss(msg.id)}
className="w-6 h-6 flex items-center justify-center rounded-md hover:bg-black/8 transition-colors ml-1"
aria-label="Скрыть"
>
<X className="w-3.5 h-3.5 text-muted-foreground" />
</button>
</div>
</div>
{/* Тело */}
<div className="flex items-start gap-3 px-3 pb-3">
<div className={`flex items-center justify-center w-8 h-8 rounded-full flex-shrink-0 ${cfg.iconWrap}`}>
{cfg.icon}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-medium text-sm mb-0.5">{notification.title}</h3>
<p className="text-xs text-muted-foreground leading-tight">{notification.description}</p>
<p className="font-medium text-sm text-foreground leading-snug mb-0.5">{msg.title}</p>
<p className="text-xs text-muted-foreground leading-snug">{msg.description}</p>
{/* Промо-цена */}
{msg.type === 'promo' && msg.promo && (
<div className="flex items-baseline gap-1.5 mt-1.5">
<span className="text-base font-bold text-[#e65100]">{msg.promo.price}</span>
{msg.promo.originalPrice && (
<span className="text-xs text-muted-foreground line-through">{msg.promo.originalPrice}</span>
)}
</div>
)}
</div>
<button
onClick={() => navigate(`/notification/${notification.id}`)}
className="text-xs text-primary font-medium flex-shrink-0 mt-1 hover:underline"
>
{notification.action}
</button>
</div>
))}
{/* Футер с действием */}
<div className="border-t border-black/6 px-3 py-2 flex items-center justify-between">
<button
onClick={handleAction}
className={`text-xs font-semibold flex items-center gap-1 ${cfg.actionColor} hover:opacity-75 transition-opacity`}
>
{msg.type === 'message' && <MessageCircle className="w-3 h-3" />}
{msg.type === 'promo' && <ArrowRight className="w-3 h-3" />}
{actionLabel}
</button>
{/* Для промо — дополнительно показываем дату */}
{msg.type === 'promo' && (
<span className="text-[11px] text-muted-foreground">{msg.date}</span>
)}
</div>
</div>
</div>
);
}
+15 -4
View File
@@ -1,11 +1,22 @@
import { Headphones } from 'lucide-react';
import { Headphones, MessageCircle } from 'lucide-react';
import { useNavigate } from 'react-router';
export function QuickActions() {
const navigate = useNavigate();
return (
<div className="px-4 pb-4">
<button className="w-full flex items-center justify-center gap-2 py-2.5 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors">
<div className="px-4 pb-4 flex gap-2">
<button
onClick={() => navigate('/chat/1')}
className="flex-1 flex items-center justify-center gap-2 py-2.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
>
<MessageCircle className="w-4 h-4" />
<span className="text-sm font-medium">Чат</span>
</button>
<button className="flex-1 flex items-center justify-center gap-2 py-2.5 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors">
<Headphones className="w-4 h-4" />
<span className="text-sm">Связь с оператором</span>
<span className="text-sm">Оператор</span>
</button>
</div>
);
+121
View File
@@ -0,0 +1,121 @@
export interface CompanyMessage {
id: string;
type: 'urgent' | 'reminder' | 'info' | 'message' | 'promo';
title: string;
description: string;
fullContent: string;
deadline?: string;
steps?: string[];
date: string;
// Для type === 'message' — ID чата куда перейти
chatId?: string;
// Для type === 'promo'
promo?: {
price: string;
originalPrice?: string;
cta: string;
};
}
export const companyMessages: CompanyMessage[] = [
{
id: '1',
type: 'urgent',
title: 'Продление патента через 14 дней',
description: 'Не забудьте подать документы заранее',
fullContent: 'Ваш патент на работу истекает 19 июня 2026 года. Рекомендуем начать процесс продления заранее, чтобы избежать перерыва в легальном статусе.',
deadline: '19 июня 2026',
steps: [
'Подготовьте пакет документов (паспорт, миграционная карта, текущий патент)',
'Оплатите госпошлину и НДФЛ',
'Подайте документы в МФЦ или МВД',
'Получите новый патент в течение 10 рабочих дней',
],
date: 'Сегодня, 09:00',
},
{
id: '2',
type: 'reminder',
title: 'Проверьте статус РВП',
description: 'Возможно, уже готово к получению',
fullContent: 'Прошло более 6 месяцев с момента подачи документов на РВП. Рекомендуем проверить готовность документа на сайте МВД или обратиться в отделение лично.',
steps: [
'Зайдите на сайт гувм.мвд.рф',
'Проверьте статус по номеру заявления',
'При готовности запишитесь на получение',
'Подготовьте документы для получения РВП',
],
date: 'Вчера, 11:30',
},
{
id: '3',
type: 'info',
title: 'Новые услуги в приложении',
description: 'Теперь можно загружать документы прямо в чат',
fullContent: 'Мы обновили приложение: теперь вы можете прикладывать фотографии документов прямо в диалог с консультантом. HAN проверит данные и подскажет, всё ли в порядке.',
date: '20 мая, 15:00',
},
{
id: '4',
type: 'message',
title: 'Ваш консультант ответил',
description: 'Готов пакет документов для подачи на ВНЖ',
fullContent: 'Консультант подготовил полный список документов для подачи на вид на жительство. Откройте чат, чтобы скачать список и задать вопросы.',
date: 'Сегодня, 12:14',
chatId: '2',
},
{
id: '5',
type: 'urgent',
title: 'Истекает регистрация — 3 дня',
description: 'Продлите регистрацию, чтобы избежать штрафа',
fullContent: 'Срок вашей регистрации по месту пребывания истекает 24 июля 2026 года. Нарушение сроков регистрации влечёт административный штраф до 5 000 ₽ и риск аннулирования патента.',
deadline: '24 июля 2026',
steps: [
'Обратитесь к собственнику жилья для продления уведомления',
'Подайте уведомление в МФЦ или МВД',
'Получите отметку о регистрации в течение 1 рабочего дня',
],
date: 'Сегодня, 08:15',
},
{
id: '7',
type: 'promo',
title: 'Полное оформление ВНЖ под ключ',
description: 'Юрист сам подаст документы — вам только расписаться',
fullContent: 'Наш партнёр «МиграЛекс» берёт на себя весь процесс: сбор документов, перевод, нотариус, подача в МВД и отслеживание статуса. Вы приходите только на получение.',
date: 'Вчера, 10:00',
promo: {
price: '8 900 ₽',
originalPrice: '14 000 ₽',
cta: 'Узнать подробнее',
},
},
];
const DISMISSED_KEY = 'han_dismissed_notifications';
export function getDismissedIds(): string[] {
try {
return JSON.parse(localStorage.getItem(DISMISSED_KEY) ?? '[]');
} catch {
return [];
}
}
export function dismissMessage(id: string) {
const current = getDismissedIds();
if (!current.includes(id)) {
localStorage.setItem(DISMISSED_KEY, JSON.stringify([...current, id]));
}
}
export function getActiveMessages(): CompanyMessage[] {
const dismissed = getDismissedIds();
return companyMessages.filter(m => !dismissed.includes(m.id));
}
export function getAllMessages(): (CompanyMessage & { dismissed: boolean })[] {
const dismissed = getDismissedIds();
return companyMessages.map(m => ({ ...m, dismissed: dismissed.includes(m.id) }));
}
+70 -66
View File
@@ -1,52 +1,33 @@
import { MessageSquare, Clock, ArrowLeft } from 'lucide-react';
import { Clock, ArrowLeft, Bell, AlertCircle, Calendar, MessageCircle, Tag } from 'lucide-react';
import { useNavigate } from 'react-router';
import { getActiveMessages, CompanyMessage } from '../data/companyMessages';
interface ChatSession {
id: string;
title: string;
lastMessage: string;
timestamp: string;
unread?: boolean;
function getIcon(type: CompanyMessage['type']) {
if (type === 'urgent') return <AlertCircle className="w-4 h-4" />;
if (type === 'reminder') return <Calendar className="w-4 h-4" />;
if (type === 'message') return <MessageCircle className="w-4 h-4" />;
if (type === 'promo') return <Tag className="w-4 h-4" />;
return <Bell className="w-4 h-4" />;
}
const chatHistory: ChatSession[] = [
{
id: '1',
title: 'Продление патента',
lastMessage: 'Спасибо за помощь! Я понял что нужно делать.',
timestamp: 'Сегодня, 14:30',
unread: false
},
{
id: '2',
title: 'Документы для РВП',
lastMessage: 'Какие документы нужны для подачи на РВП?',
timestamp: 'Вчера, 18:45',
unread: false
},
{
id: '3',
title: 'Регистрация по месту жительства',
lastMessage: 'HAN: Вам нужно обратиться в МФЦ с паспортом и...',
timestamp: '20 мая, 10:15',
unread: false
},
{
id: '4',
title: 'Оплата патента',
lastMessage: 'Где можно оплатить патент?',
timestamp: '18 мая, 16:20',
unread: false
},
];
function typeColors(type: CompanyMessage['type']) {
if (type === 'urgent') return { icon: 'bg-destructive/12 text-destructive', dot: 'bg-destructive' };
if (type === 'reminder') return { icon: 'bg-primary/10 text-primary', dot: 'bg-primary' };
if (type === 'message') return { icon: 'bg-[#c8e6c9] text-[#2e7d32]', dot: 'bg-[#43a047]' };
if (type === 'promo') return { icon: 'bg-[#fff3cd] text-[#e65100]', dot: 'bg-[#fb8c00]' };
return { icon: 'bg-muted text-muted-foreground', dot: 'bg-muted-foreground' };
}
export function History() {
const navigate = useNavigate();
const messages = getActiveMessages();
return (
<div className="flex-1 overflow-y-auto">
<div className="px-4 py-4">
<div className="flex items-center gap-3 mb-4">
<div className="px-4 py-4 space-y-4">
{/* Заголовок страницы */}
<div className="flex items-center gap-3">
<button
onClick={() => navigate('/')}
className="flex items-center justify-center w-9 h-9 rounded-full hover:bg-muted transition-colors"
@@ -54,35 +35,58 @@ export function History() {
>
<ArrowLeft className="w-5 h-5" />
</button>
<div>
<h1 className="text-xl font-medium">История общения</h1>
<p className="text-sm text-muted-foreground">Все ваши диалоги с HAN</p>
</div>
<h1 className="flex-1 text-xl font-medium">Центр уведомлений</h1>
{messages.length > 0 && (
<span className="text-xs font-medium text-primary bg-primary/10 rounded-full px-2.5 py-1">
{messages.length}
</span>
)}
</div>
<div className="space-y-2">
{chatHistory.map((chat) => (
<button
key={chat.id}
onClick={() => navigate(`/chat/${chat.id}`)}
className="w-full bg-card border border-border rounded-lg p-4 flex items-start gap-3 hover:bg-accent/50 transition-colors text-left"
>
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10 text-primary flex-shrink-0">
<MessageSquare className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-medium text-sm mb-1">{chat.title}</h3>
<p className="text-xs text-muted-foreground line-clamp-1 mb-2">
{chat.lastMessage}
</p>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="w-3 h-3" />
<span>{chat.timestamp}</span>
</div>
</div>
</button>
))}
</div>
{/* Список уведомлений */}
{messages.length > 0 ? (
<div className="space-y-2">
{messages.map(msg => {
const colors = typeColors(msg.type);
return (
<button
key={msg.id}
onClick={() => msg.type === 'message' && msg.chatId
? navigate(`/chat/${msg.chatId}`)
: navigate(`/notification/${msg.id}`)
}
className="w-full bg-card border border-border rounded-xl p-3.5 flex items-start gap-3 hover:bg-accent/50 transition-colors text-left"
>
<div className={`relative flex items-center justify-center w-9 h-9 rounded-full flex-shrink-0 ${colors.icon}`}>
{getIcon(msg.type)}
<span className={`absolute -top-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-background ${colors.dot}`} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground leading-snug line-clamp-1 mb-0.5">
{msg.title}
</p>
<p className="text-xs text-muted-foreground line-clamp-1 mb-1.5">
{msg.description}
</p>
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
<Clock className="w-3 h-3" />
<span>{msg.date}</span>
</div>
</div>
</button>
);
})}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-12 h-12 rounded-full bg-muted flex items-center justify-center mb-3">
<Bell className="w-5 h-5 text-muted-foreground" />
</div>
<p className="text-sm font-medium text-foreground mb-1">Всё актуально</p>
<p className="text-xs text-muted-foreground">Новых уведомлений нет</p>
</div>
)}
</div>
</div>
);
+50 -78
View File
@@ -1,89 +1,70 @@
import { useNavigate, useParams } from 'react-router';
import { ArrowLeft, Calendar, FileText, CheckCircle, EyeOff, Clock } from 'lucide-react';
import { ArrowLeft, Calendar, AlertCircle, Bell, CheckCircle, EyeOff, Clock } from 'lucide-react';
import { companyMessages, dismissMessage } from '../data/companyMessages';
interface NotificationData {
id: string;
type: 'urgent' | 'reminder';
icon: React.ReactNode;
title: string;
description: string;
fullContent: string;
deadline?: string;
steps?: string[];
function getIcon(type: 'urgent' | 'reminder' | 'info', className = 'w-5 h-5') {
if (type === 'urgent') return <AlertCircle className={className} />;
if (type === 'reminder') return <Calendar className={className} />;
return <Bell className={className} />;
}
const notificationDetails: Record<string, NotificationData> = {
'1': {
id: '1',
type: 'urgent',
icon: <Calendar className="w-6 h-6" />,
title: 'Продление патента через 14 дней',
description: 'Не забудьте подать документы заранее',
fullContent: 'Ваш патент на работу истекает 19 июня 2026 года. Рекомендуем начать процесс продления заранее, чтобы избежать перерыва в легальном статусе.',
deadline: '19 июня 2026',
steps: [
'Подготовьте пакет документов (паспорт, миграционная карта, текущий патент)',
'Оплатите госпошлину и НДФЛ',
'Подайте документы в МФЦ или МВД',
'Получите новый патент в течение 10 рабочих дней'
]
},
'2': {
id: '2',
type: 'reminder',
icon: <FileText className="w-6 h-6" />,
title: 'Проверьте статус РВП',
description: 'Возможно, уже готово к получению',
fullContent: 'Прошло более 6 месяцев с момента подачи документов на РВП. Рекомендуем проверить готовность документа на сайте МВД или обратиться в отделение лично.',
steps: [
'Зайдите на сайт гувм.мвд.рф',
'Проверьте статус по номеру заявления',
'При готовности запишитесь на получение',
'Подготовьте документы для получения РВП'
]
}
};
function typeStyle(type: 'urgent' | 'reminder' | 'info') {
if (type === 'urgent') return {
wrapper: 'bg-destructive/10 text-destructive',
deadline: 'bg-destructive/6 border-destructive/20',
deadlineText: 'text-destructive',
step: 'bg-destructive/10 text-destructive',
};
if (type === 'reminder') return {
wrapper: 'bg-primary/10 text-primary',
deadline: 'bg-primary/5 border-primary/20',
deadlineText: 'text-primary',
step: 'bg-primary/10 text-primary',
};
return {
wrapper: 'bg-muted text-muted-foreground',
deadline: 'bg-muted border-border',
deadlineText: 'text-foreground',
step: 'bg-muted text-muted-foreground',
};
}
export function NotificationDetail() {
const navigate = useNavigate();
const { id } = useParams();
const notification = id ? notificationDetails[id] : null;
const notification = id ? companyMessages.find(m => m.id === id) : null;
if (!notification) {
return (
<div className="flex flex-col items-center justify-center h-full p-4">
<p className="text-muted-foreground">Уведомление не найдено</p>
<button
onClick={() => navigate('/')}
className="mt-4 text-primary font-medium"
>
<button onClick={() => navigate('/')} className="mt-4 text-primary font-medium">
Вернуться на главную
</button>
</div>
);
}
const style = typeStyle(notification.type);
const handleComplete = () => {
// В реальном приложении здесь будет логика отметки уведомления как выполненного
dismissMessage(notification.id);
navigate('/');
};
const handleHide = () => {
// В реальном приложении здесь будет логика скрытия уведомления
dismissMessage(notification.id);
navigate('/');
};
const handleLater = () => {
// В реальном приложении здесь будет логика напоминания позже
navigate('/');
};
const handleLater = () => navigate('/');
return (
<div className="flex flex-col h-full">
{/* Хедер с кнопкой назад */}
{/* Хедер */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-border/40">
<button
onClick={() => navigate('/')}
onClick={() => navigate(-1)}
className="p-1.5 -ml-1.5 hover:bg-muted/50 rounded-lg transition-colors"
>
<ArrowLeft className="w-5 h-5" />
@@ -96,12 +77,8 @@ export function NotificationDetail() {
<div className="p-4 space-y-4">
{/* Иконка и заголовок */}
<div className="flex items-start gap-3">
<div className={`flex items-center justify-center w-12 h-12 rounded-full flex-shrink-0 ${
notification.type === 'urgent'
? 'bg-primary/10 text-primary'
: 'bg-muted text-muted-foreground'
}`}>
{notification.icon}
<div className={`flex items-center justify-center w-12 h-12 rounded-full flex-shrink-0 ${style.wrapper}`}>
{getIcon(notification.type, 'w-6 h-6')}
</div>
<div className="flex-1 min-w-0 pt-1">
<h2 className="font-semibold text-base mb-1">{notification.title}</h2>
@@ -109,13 +86,13 @@ export function NotificationDetail() {
</div>
</div>
{/* Срок, если есть */}
{/* Дедлайн */}
{notification.deadline && (
<div className="bg-primary/5 border border-primary/20 rounded-lg p-3">
<div className={`border rounded-lg p-3 ${style.deadline}`}>
<div className="flex items-center gap-2 text-sm">
<Calendar className="w-4 h-4 text-primary" />
<Calendar className={`w-4 h-4 ${style.deadlineText}`} />
<span className="font-medium">Срок:</span>
<span className="text-primary font-semibold">{notification.deadline}</span>
<span className={`font-semibold ${style.deadlineText}`}>{notification.deadline}</span>
</div>
</div>
)}
@@ -123,24 +100,20 @@ export function NotificationDetail() {
{/* Полное описание */}
<div className="space-y-2">
<h3 className="font-medium text-sm">Подробности</h3>
<p className="text-sm text-foreground/90 leading-relaxed">
{notification.fullContent}
</p>
<p className="text-sm text-foreground/90 leading-relaxed">{notification.fullContent}</p>
</div>
{/* Шаги к действию */}
{/* Шаги */}
{notification.steps && notification.steps.length > 0 && (
<div className="space-y-2">
<h3 className="font-medium text-sm">Что нужно сделать</h3>
<div className="space-y-2">
{notification.steps.map((step, index) => (
<div key={index} className="flex gap-2.5">
<div className="flex items-center justify-center w-5 h-5 rounded-full bg-primary/10 text-primary text-xs font-medium flex-shrink-0 mt-0.5">
{index + 1}
{notification.steps.map((step, i) => (
<div key={i} className="flex gap-2.5">
<div className={`flex items-center justify-center w-5 h-5 rounded-full text-xs font-medium flex-shrink-0 mt-0.5 ${style.step}`}>
{i + 1}
</div>
<p className="text-sm text-foreground/90 leading-relaxed flex-1">
{step}
</p>
<p className="text-sm text-foreground/90 leading-relaxed flex-1">{step}</p>
</div>
))}
</div>
@@ -149,7 +122,7 @@ export function NotificationDetail() {
</div>
</div>
{/* Кнопки управления */}
{/* Кнопки */}
<div className="border-t border-border/40 p-4 space-y-2 bg-background">
<button
onClick={handleComplete}
@@ -167,13 +140,12 @@ export function NotificationDetail() {
<Clock className="w-4 h-4" />
Сделаю позже
</button>
<button
onClick={handleHide}
className="flex-1 bg-muted text-muted-foreground rounded-lg px-4 py-2.5 text-sm font-medium flex items-center justify-center gap-2 hover:bg-muted/80 transition-colors"
>
<EyeOff className="w-4 h-4" />
Скрыть уведомление
Скрыть
</button>
</div>
</div>