feat: travel service with 2GIS routing, POI, hotels + finance providers + UI overhaul

- Add travel-svc microservice (Amadeus, TravelPayouts, 2GIS, OpenRouteService)
- Add travel orchestrator with parallel collectors (events, POI, hotels, flights)
- Add 2GIS road routing with transport cost calculation (car/bus/taxi)
- Add TravelMap (2GIS MapGL) and TravelWidgets components
- Add useTravelChat hook for streaming travel agent responses
- Add finance heatmap providers refactor
- Add SearXNG settings, API proxy routes, Docker compose updates
- Update Dockerfiles, config, types, and all UI pages for consistency

Made-with: Cursor
This commit is contained in:
home
2026-03-01 21:58:32 +03:00
parent e6b9cfc60a
commit 08bd41e75c
71 changed files with 12364 additions and 945 deletions

View File

@@ -83,15 +83,15 @@ export default function AdminAuditPage() {
const getActionColor = (action: string) => {
switch (action) {
case 'create':
return 'bg-green-500/10 text-green-400';
return 'bg-green-500/10 text-green-600';
case 'update':
return 'bg-blue-500/10 text-blue-400';
return 'bg-blue-500/10 text-blue-600';
case 'delete':
return 'bg-red-500/10 text-red-400';
return 'bg-red-500/10 text-red-600';
case 'publish':
return 'bg-purple-500/10 text-purple-400';
return 'bg-purple-500/10 text-purple-600';
default:
return 'bg-gray-500/10 text-gray-400';
return 'bg-surface text-muted';
}
};

View File

@@ -48,8 +48,8 @@ function CategoryModal({ category, onClose, onSave }: CategoryModalProps) {
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-surface rounded-xl p-6 w-full max-w-md">
<div className="fixed inset-0 bg-primary/25 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-elevated rounded-xl p-6 w-full max-w-md shadow-dropdown border border-border">
<h2 className="text-xl font-bold text-primary mb-4">
{category ? 'Редактировать категорию' : 'Новая категория'}
</h2>
@@ -138,7 +138,7 @@ function CategoryModal({ category, onClose, onSave }: CategoryModalProps) {
</button>
<button
type="submit"
className="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
className="flex-1 px-4 py-2 bg-accent text-accent-foreground rounded-lg hover:bg-accent-hover transition-colors"
>
Сохранить
</button>
@@ -175,8 +175,8 @@ function SourceModal({ onClose, onSave }: SourceModalProps) {
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-surface rounded-xl p-6 w-full max-w-md">
<div className="fixed inset-0 bg-primary/25 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-elevated rounded-xl p-6 w-full max-w-md shadow-dropdown border border-border">
<h2 className="text-xl font-bold text-primary mb-4">Новый источник</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
@@ -254,7 +254,7 @@ function SourceModal({ onClose, onSave }: SourceModalProps) {
</button>
<button
type="submit"
className="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
className="flex-1 px-4 py-2 bg-accent text-accent-foreground rounded-lg hover:bg-accent-hover transition-colors"
>
Добавить
</button>
@@ -390,7 +390,7 @@ export default function AdminDiscoverPage() {
<p className="text-sm text-muted">Перетащите для изменения порядка</p>
<button
onClick={() => setCategoryModal(null)}
className="flex items-center gap-2 px-3 py-1.5 bg-primary text-white rounded-lg text-sm hover:bg-primary/90 transition-colors"
className="flex items-center gap-2 px-3 py-1.5 bg-accent text-accent-foreground rounded-lg text-sm hover:bg-accent-hover transition-colors"
>
<Plus className="w-4 h-4" />
Добавить
@@ -414,7 +414,7 @@ export default function AdminDiscoverPage() {
<div className="flex items-center gap-2">
<button
onClick={() => handleToggleCategory(cat)}
className={`p-1 rounded ${cat.isActive ? 'text-green-400' : 'text-muted'}`}
className={`p-1 rounded ${cat.isActive ? 'text-green-600' : 'text-muted'}`}
>
{cat.isActive ? <ToggleRight className="w-6 h-6" /> : <ToggleLeft className="w-6 h-6" />}
</button>
@@ -426,7 +426,7 @@ export default function AdminDiscoverPage() {
</button>
<button
onClick={() => handleDeleteCategory(cat.id)}
className="p-1 text-muted hover:text-red-400 rounded"
className="p-1 text-muted hover:text-red-600 rounded"
>
<Trash2 className="w-4 h-4" />
</button>
@@ -443,7 +443,7 @@ export default function AdminDiscoverPage() {
<p className="text-sm text-muted">Доверенные источники новостей</p>
<button
onClick={() => setShowSourceModal(true)}
className="flex items-center gap-2 px-3 py-1.5 bg-primary text-white rounded-lg text-sm hover:bg-primary/90 transition-colors"
className="flex items-center gap-2 px-3 py-1.5 bg-accent text-accent-foreground rounded-lg text-sm hover:bg-accent-hover transition-colors"
>
<Plus className="w-4 h-4" />
Добавить
@@ -474,15 +474,15 @@ export default function AdminDiscoverPage() {
<div className="text-sm">
<span className="text-muted">Доверие:</span>
<span className={`ml-1 font-medium ${
source.trustScore >= 0.7 ? 'text-green-400' :
source.trustScore >= 0.4 ? 'text-yellow-400' : 'text-red-400'
source.trustScore >= 0.7 ? 'text-green-600' :
source.trustScore >= 0.4 ? 'text-yellow-600' : 'text-red-600'
}`}>
{(source.trustScore * 100).toFixed(0)}%
</span>
</div>
<button
onClick={() => handleDeleteSource(source.id)}
className="p-1 text-muted hover:text-red-400 rounded"
className="p-1 text-muted hover:text-red-600 rounded"
>
<Trash2 className="w-4 h-4" />
</button>

View File

@@ -69,7 +69,7 @@ export default function AdminDashboardPage() {
if (error) {
return (
<div className="bg-red-500/10 text-red-400 p-4 rounded-lg">
<div className="bg-red-500/10 text-red-600 p-4 rounded-lg">
{error}
</div>
);

View File

@@ -46,7 +46,7 @@ function PostModal({ post, onClose, onSave }: PostModalProps) {
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="fixed inset-0 bg-primary/25 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-surface rounded-xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<h2 className="text-xl font-bold text-primary mb-4">
{post ? 'Редактировать пост' : 'Новый пост'}
@@ -129,7 +129,7 @@ function PostModal({ post, onClose, onSave }: PostModalProps) {
</button>
<button
type="submit"
className="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
className="flex-1 px-4 py-2 bg-accent text-accent-foreground rounded-lg hover:bg-accent-hover transition-colors"
>
Сохранить
</button>
@@ -204,13 +204,13 @@ export default function AdminPostsPage() {
const getStatusBadge = (status: string) => {
switch (status) {
case 'published':
return 'bg-green-500/10 text-green-400';
return 'bg-green-500/10 text-green-600';
case 'draft':
return 'bg-yellow-500/10 text-yellow-400';
return 'bg-yellow-500/10 text-yellow-600';
case 'archived':
return 'bg-gray-500/10 text-gray-400';
return 'bg-surface text-muted';
default:
return 'bg-gray-500/10 text-gray-400';
return 'bg-surface text-muted';
}
};
@@ -236,7 +236,7 @@ export default function AdminPostsPage() {
</div>
<button
onClick={() => setModalPost(null)}
className="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
className="flex items-center gap-2 px-4 py-2 bg-accent text-accent-foreground rounded-lg hover:bg-accent-hover transition-colors"
>
<Plus className="w-4 h-4" />
Создать
@@ -338,7 +338,7 @@ export default function AdminPostsPage() {
handlePublish(post.id);
setActiveMenu(null);
}}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-green-400 hover:bg-base"
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-green-600 hover:bg-base"
>
<Send className="w-4 h-4" />
Опубликовать
@@ -358,7 +358,7 @@ export default function AdminPostsPage() {
handleDelete(post.id);
setActiveMenu(null);
}}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-400 hover:bg-base"
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 hover:bg-base"
>
<Trash2 className="w-4 h-4" />
Удалить

View File

@@ -121,7 +121,7 @@ export default function AdminSettingsPage() {
if (!settings || !features) {
return (
<div className="bg-red-500/10 text-red-400 p-4 rounded-lg">
<div className="bg-red-500/10 text-red-600 p-4 rounded-lg">
Не удалось загрузить настройки
</div>
);
@@ -224,7 +224,7 @@ export default function AdminSettingsPage() {
<button
onClick={handleSaveGeneral}
disabled={saving}
className="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50"
className="flex items-center gap-2 px-4 py-2 bg-accent text-accent-foreground rounded-lg hover:bg-accent-hover transition-colors disabled:opacity-50"
>
<Save className="w-4 h-4" />
{saving ? 'Сохранение...' : 'Сохранить'}
@@ -252,7 +252,7 @@ export default function AdminSettingsPage() {
</div>
<button
onClick={() => toggleFeature(feature.key as keyof FeatureFlags)}
className={features[feature.key as keyof FeatureFlags] ? 'text-green-400' : 'text-muted'}
className={features[feature.key as keyof FeatureFlags] ? 'text-green-600' : 'text-muted'}
>
{features[feature.key as keyof FeatureFlags] ? (
<ToggleRight className="w-8 h-8" />
@@ -267,7 +267,7 @@ export default function AdminSettingsPage() {
<button
onClick={handleSaveFeatures}
disabled={saving}
className="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50"
className="flex items-center gap-2 px-4 py-2 bg-accent text-accent-foreground rounded-lg hover:bg-accent-hover transition-colors disabled:opacity-50"
>
<Save className="w-4 h-4" />
{saving ? 'Сохранение...' : 'Сохранить'}
@@ -345,7 +345,7 @@ export default function AdminSettingsPage() {
<button
onClick={handleSaveLLM}
disabled={saving}
className="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50"
className="flex items-center gap-2 px-4 py-2 bg-accent text-accent-foreground rounded-lg hover:bg-accent-hover transition-colors disabled:opacity-50"
>
<Save className="w-4 h-4" />
{saving ? 'Сохранение...' : 'Сохранить'}
@@ -397,7 +397,7 @@ export default function AdminSettingsPage() {
...settings,
searchSettings: { ...settings.searchSettings, safeSearch: !settings.searchSettings.safeSearch }
})}
className={settings.searchSettings.safeSearch ? 'text-green-400' : 'text-muted'}
className={settings.searchSettings.safeSearch ? 'text-green-600' : 'text-muted'}
>
{settings.searchSettings.safeSearch ? (
<ToggleRight className="w-8 h-8" />
@@ -411,7 +411,7 @@ export default function AdminSettingsPage() {
<button
onClick={handleSaveSearch}
disabled={saving}
className="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50"
className="flex items-center gap-2 px-4 py-2 bg-accent text-accent-foreground rounded-lg hover:bg-accent-hover transition-colors disabled:opacity-50"
>
<Save className="w-4 h-4" />
{saving ? 'Сохранение...' : 'Сохранить'}

View File

@@ -44,7 +44,7 @@ function UserModal({ user, onClose, onSave }: UserModalProps) {
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="fixed inset-0 bg-primary/25 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-surface rounded-xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold text-primary mb-4">
{user ? 'Редактировать пользователя' : 'Новый пользователь'}
@@ -117,7 +117,7 @@ function UserModal({ user, onClose, onSave }: UserModalProps) {
</button>
<button
type="submit"
className="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
className="flex-1 px-4 py-2 bg-accent text-accent-foreground rounded-lg hover:bg-accent-hover transition-colors"
>
Сохранить
</button>
@@ -198,7 +198,7 @@ export default function AdminUsersPage() {
</div>
<button
onClick={() => setModalUser(null)}
className="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
className="flex items-center gap-2 px-4 py-2 bg-accent text-accent-foreground rounded-lg hover:bg-accent-hover transition-colors"
>
<Plus className="w-4 h-4" />
Добавить
@@ -263,8 +263,8 @@ export default function AdminUsersPage() {
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs ${
user.role === 'admin'
? 'bg-purple-500/10 text-purple-400'
: 'bg-blue-500/10 text-blue-400'
? 'bg-purple-500/10 text-purple-600'
: 'bg-blue-500/10 text-blue-600'
}`}>
{user.role === 'admin' && <Crown className="w-3 h-3" />}
{user.role === 'admin' ? 'Админ' : 'Пользователь'}
@@ -273,10 +273,10 @@ export default function AdminUsersPage() {
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-xs ${
user.tier === 'business'
? 'bg-yellow-500/10 text-yellow-400'
? 'bg-yellow-500/10 text-yellow-600'
: user.tier === 'pro'
? 'bg-green-500/10 text-green-400'
: 'bg-gray-500/10 text-gray-400'
? 'bg-green-500/10 text-green-600'
: 'bg-surface text-muted'
}`}>
{user.tier.toUpperCase()}
</span>
@@ -284,8 +284,8 @@ export default function AdminUsersPage() {
<td className="px-4 py-3">
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs ${
user.isActive
? 'bg-green-500/10 text-green-400'
: 'bg-red-500/10 text-red-400'
? 'bg-green-500/10 text-green-600'
: 'bg-red-500/10 text-red-600'
}`}>
{user.isActive ? <UserCheck className="w-3 h-3" /> : <UserX className="w-3 h-3" />}
{user.isActive ? 'Активен' : 'Неактивен'}
@@ -329,7 +329,7 @@ export default function AdminUsersPage() {
handleDelete(user.id);
setActiveMenu(null);
}}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-400 hover:bg-base"
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 hover:bg-base"
>
<Trash2 className="w-4 h-4" />
Удалить

View File

@@ -241,7 +241,7 @@ export default function FinancePage() {
</span>
</div>
<div className="space-y-1">
{sector.tickers.slice(0, 3).map((stock, i) => (
{(sector.tickers ?? []).slice(0, 3).map((stock, i) => (
<StockRow key={stock.symbol} stock={stock} market={currentMarket} delay={i * 0.02} />
))}
</div>

View File

@@ -80,9 +80,9 @@ const healthArticles: Article[] = [
];
const quickServices = [
{ icon: Stethoscope, label: 'Найти врача', color: 'bg-blue-500/10 text-blue-400' },
{ icon: Pill, label: 'Справочник лекарств', color: 'bg-green-500/10 text-green-400' },
{ icon: FileText, label: 'Анализы', color: 'bg-purple-500/10 text-purple-400' },
{ icon: Stethoscope, label: 'Найти врача', color: 'bg-blue-500/10 text-blue-600' },
{ icon: Pill, label: 'Справочник лекарств', color: 'bg-green-500/10 text-green-600' },
{ icon: FileText, label: 'Анализы', color: 'bg-purple-500/10 text-purple-600' },
{ icon: Sparkles, label: 'AI Консультант', color: 'active-gradient text-gradient' },
];
@@ -159,9 +159,9 @@ export default function MedicinePage() {
{/* Disclaimer */}
<div className="flex items-start gap-3 p-4 bg-amber-500/5 border border-amber-500/20 rounded-xl mb-6">
<AlertTriangle className="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5" />
<AlertTriangle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-400 mb-1">Важно</p>
<p className="text-sm font-medium text-amber-600 mb-1">Важно</p>
<p className="text-xs text-muted">
Информация носит справочный характер и не заменяет консультацию врача.
При серьёзных симптомах обратитесь к специалисту.
@@ -236,7 +236,7 @@ export default function MedicinePage() {
<div className="mb-6 sm:mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-9 h-9 rounded-xl bg-red-500/10 flex items-center justify-center">
<HeartPulse className="w-4 h-4 text-red-400" />
<HeartPulse className="w-4 h-4 text-red-600" />
</div>
<h2 className="text-sm font-medium text-primary">Частые симптомы</h2>
</div>
@@ -256,7 +256,7 @@ export default function MedicinePage() {
<div className="mb-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-9 h-9 rounded-xl bg-green-500/10 flex items-center justify-center">
<FileText className="w-4 h-4 text-green-400" />
<FileText className="w-4 h-4 text-green-600" />
</div>
<h2 className="text-sm font-medium text-primary">Полезные статьи</h2>
</div>
@@ -271,8 +271,8 @@ export default function MedicinePage() {
{/* Emergency Info */}
<div className="bg-red-500/5 border border-red-500/20 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-red-400" />
<span className="text-sm font-medium text-red-400">Экстренная помощь</span>
<AlertTriangle className="w-4 h-4 text-red-600" />
<span className="text-sm font-medium text-red-600">Экстренная помощь</span>
</div>
<p className="text-sm text-muted mb-3">
При угрозе жизни немедленно вызывайте скорую помощь
@@ -280,13 +280,13 @@ export default function MedicinePage() {
<div className="flex gap-3">
<a
href="tel:103"
className="flex-1 h-10 flex items-center justify-center gap-2 bg-red-500/10 border border-red-500/30 text-red-400 font-medium text-sm rounded-lg hover:bg-red-500/20 transition-colors"
className="flex-1 h-10 flex items-center justify-center gap-2 bg-red-500/10 border border-red-500/30 text-red-600 font-medium text-sm rounded-lg hover:bg-red-500/20 transition-colors"
>
📞 103
</a>
<a
href="tel:112"
className="flex-1 h-10 flex items-center justify-center gap-2 bg-red-500/10 border border-red-500/30 text-red-400 font-medium text-sm rounded-lg hover:bg-red-500/20 transition-colors"
className="flex-1 h-10 flex items-center justify-center gap-2 bg-red-500/10 border border-red-500/30 text-red-600 font-medium text-sm rounded-lg hover:bg-red-500/20 transition-colors"
>
📞 112
</a>

View File

@@ -335,7 +335,7 @@ export default function SpaceDetailPage() {
</span>
<span className={`flex items-center gap-1 text-xs px-2 py-0.5 rounded-full ${
member.role === 'owner'
? 'bg-amber-500/20 text-amber-400'
? 'bg-amber-500/15 text-amber-600'
: member.role === 'admin'
? 'bg-accent/20 text-accent'
: 'bg-surface text-muted'
@@ -394,7 +394,7 @@ export default function SpaceDetailPage() {
{/* Invite Modal */}
<Dialog.Root open={showInviteModal} onOpenChange={setShowInviteModal}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50" />
<Dialog.Overlay className="fixed inset-0 bg-primary/30 backdrop-blur-sm z-50" />
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-elevated border border-border rounded-2xl p-6 z-50 shadow-2xl">
<div className="flex items-center justify-between mb-6">
<Dialog.Title className="text-lg font-semibold text-primary">

View File

@@ -105,7 +105,7 @@ export default function NewSpacePage() {
`}
>
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-xl bg-white/10 backdrop-blur-sm flex items-center justify-center text-2xl">
<div className="w-14 h-14 rounded-xl bg-surface/60 backdrop-blur-sm flex items-center justify-center text-2xl">
{formData.icon}
</div>
<div>
@@ -183,7 +183,7 @@ export default function NewSpacePage() {
onClick={() => setFormData((f) => ({ ...f, color: color.id }))}
className={`w-10 h-10 rounded-xl ${color.class} transition-all ${
formData.color === color.id
? 'ring-2 ring-white ring-offset-2 ring-offset-base'
? 'ring-2 ring-accent ring-offset-2 ring-offset-base'
: 'opacity-60 hover:opacity-100'
}`}
title={color.label}
@@ -237,7 +237,7 @@ export default function NewSpacePage() {
}`}
>
<span
className={`absolute top-1 w-5 h-5 rounded-full bg-white shadow transition-transform ${
className={`absolute top-1 w-5 h-5 rounded-full bg-elevated shadow transition-transform ${
formData.isPublic ? 'translate-x-6' : 'translate-x-1'
}`}
/>

View File

@@ -160,7 +160,7 @@ export default function SpacesPage() {
>
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 rounded-xl bg-white/10 backdrop-blur-sm flex items-center justify-center">
<div className="w-12 h-12 rounded-xl bg-surface/60 backdrop-blur-sm flex items-center justify-center">
<FolderOpen className="w-6 h-6 text-primary" />
</div>
<div className="flex items-center gap-1">
@@ -173,7 +173,7 @@ export default function SpacesPage() {
<DropdownMenu.Trigger asChild>
<button
onClick={(e) => e.preventDefault()}
className="p-1.5 hover:bg-white/10 rounded-lg transition-colors opacity-0 group-hover:opacity-100"
className="p-1.5 hover:bg-surface/50 rounded-lg transition-colors opacity-0 group-hover:opacity-100"
>
<MoreHorizontal className="w-4 h-4 text-secondary" />
</button>
@@ -229,7 +229,7 @@ export default function SpacesPage() {
{/* Members avatars */}
{space.members && space.members.length > 0 && (
<div className="flex items-center gap-1 mt-4 pt-4 border-t border-white/10">
<div className="flex items-center gap-1 mt-4 pt-4 border-t border-border/50">
<div className="flex -space-x-2">
{space.members.slice(0, 4).map((member, idx) => (
<div

File diff suppressed because it is too large Load Diff