705 lines
34 KiB
TypeScript
705 lines
34 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
||
import { useLocation, useNavigate } from 'react-router-dom';
|
||
import { Badge, Button, MaturityBadge, useToast } from '../../shared/ui';
|
||
import {
|
||
Globe,
|
||
Smartphone,
|
||
Tablet,
|
||
Monitor,
|
||
Plus,
|
||
Edit3,
|
||
Image,
|
||
Trash2,
|
||
ArrowUp,
|
||
ArrowDown,
|
||
Settings2,
|
||
Download,
|
||
Layers,
|
||
} from 'lucide-react';
|
||
import { ulid } from '../../shared/lib/ulid';
|
||
|
||
interface CmsPage {
|
||
id: string;
|
||
title: string;
|
||
slug: string;
|
||
viewsToday: number;
|
||
status: 'published' | 'draft';
|
||
lastEdited: string;
|
||
}
|
||
|
||
export type WidgetType = 'hero' | 'features' | 'pricing' | 'testimonials' | 'form' | 'cta';
|
||
|
||
export interface CmsWidget {
|
||
id: string;
|
||
type: WidgetType;
|
||
title: string;
|
||
subtitle: string;
|
||
ctaText?: string;
|
||
badge?: string;
|
||
accentColor?: string;
|
||
}
|
||
|
||
const INITIAL_WIDGETS: CmsWidget[] = [
|
||
{
|
||
id: 'w-hero',
|
||
type: 'hero',
|
||
title: 'Свежие роллы и паназиатская кухня в Новосибирске',
|
||
subtitle: 'Доставка за 35 минут или ролл в подарок при следующем заказе',
|
||
ctaText: 'Заказать онлайн со скидкой 20%',
|
||
badge: 'Спецпредложение осени',
|
||
},
|
||
{
|
||
id: 'w-features',
|
||
type: 'features',
|
||
title: 'Почему выбирают Суши & Гёдза',
|
||
subtitle: 'Только охлажденный лосось с Фарерских островов и авторские рецепты шефа',
|
||
badge: 'Качество',
|
||
},
|
||
{
|
||
id: 'w-pricing',
|
||
type: 'pricing',
|
||
title: 'Популярные сеты для компаний',
|
||
subtitle: 'Выгодные комбо-наборы со скидкой до 30%',
|
||
ctaText: 'Выбрать сет',
|
||
badge: 'Хиты продаж',
|
||
},
|
||
{
|
||
id: 'w-testimonials',
|
||
type: 'testimonials',
|
||
title: 'Отзывы наших постоянных гостей',
|
||
subtitle: 'Более 4 800 довольных клиентов на Flamp и 2ГИС',
|
||
badge: 'Рейтинг 4.9',
|
||
},
|
||
{
|
||
id: 'w-form',
|
||
type: 'form',
|
||
title: 'Забронируйте столик на вечер',
|
||
subtitle: 'Мы подготовим стол и подарим фирменный десерт Моти',
|
||
ctaText: 'Подтвердить бронь',
|
||
badge: 'Быстрая бронь',
|
||
},
|
||
{
|
||
id: 'w-cta',
|
||
type: 'cta',
|
||
title: 'Установите приложение или оформите в Telegram',
|
||
subtitle: 'Получите 500 приветственных бонусов на первый заказ',
|
||
ctaText: 'Открыть Telegram-бот',
|
||
badge: 'Мобильный бонус',
|
||
},
|
||
];
|
||
|
||
const PAGES_LIST: CmsPage[] = [
|
||
{ id: 'p1', title: 'Главная страница доставки', slug: '/', viewsToday: 1420, status: 'published', lastEdited: 'Вчера, 18:40' },
|
||
{ id: 'p2', title: 'Меню & Корзина онлайн', slug: '/menu', viewsToday: 3890, status: 'published', lastEdited: '08.09.2026' },
|
||
{ id: 'p3', title: 'Акции и Сеты сентября', slug: '/promo-autumn', viewsToday: 840, status: 'published', lastEdited: '09.09.2026' },
|
||
{ id: 'p4', title: 'Бронирование столов / запись', slug: '/booking', viewsToday: 410, status: 'published', lastEdited: '05.09.2026' },
|
||
{ id: 'p5', title: 'Контакты и точки в городе', slug: '/contacts', viewsToday: 230, status: 'published', lastEdited: '01.09.2026' },
|
||
];
|
||
|
||
export const CmsModule: React.FC = () => {
|
||
const location = useLocation();
|
||
const navigate = useNavigate();
|
||
const { showToast } = useToast();
|
||
|
||
const [pages] = useState<CmsPage[]>(PAGES_LIST);
|
||
const [selectedTab, setSelectedTab] = useState<'pages' | 'builder' | 'forms' | 'media' | 'seo' | 'domains'>('pages');
|
||
|
||
// Page Builder States
|
||
const [widgets, setWidgets] = useState<CmsWidget[]>(INITIAL_WIDGETS);
|
||
const [selectedWidgetId, setSelectedWidgetId] = useState<string>(INITIAL_WIDGETS[0].id);
|
||
const [viewport, setViewport] = useState<'desktop' | 'tablet' | 'mobile'>('desktop');
|
||
|
||
// SEO Form Mock State
|
||
const [seoTitle, setSeoTitle] = useState('Суши & Гёдза — Доставка роллов и паназиатской кухни в Новосибирске');
|
||
const [seoDescription, setSeoDescription] = useState('Свежие суши, роллы, воки и супы с быстрой доставкой за 35 минут. Заказывайте любимые сеты онлайн со скидкой 20%!');
|
||
|
||
const activeTab = useMemo(() => {
|
||
const p = location.pathname;
|
||
if (p.includes('/cms/forms')) return 'forms';
|
||
if (p.includes('/cms/media')) return 'media';
|
||
if (p.includes('/cms/seo')) return 'seo';
|
||
if (p.includes('/cms/domains')) return 'domains';
|
||
if (p.includes('/cms/builder')) return 'builder';
|
||
if (p.includes('/cms/pages')) return 'pages';
|
||
return selectedTab;
|
||
}, [location.pathname, selectedTab]);
|
||
|
||
const setActiveTab = setSelectedTab;
|
||
|
||
const selectedWidget = widgets.find((w) => w.id === selectedWidgetId) || widgets[0];
|
||
|
||
const handleAddWidget = (type: WidgetType) => {
|
||
const typeTitles: Record<WidgetType, { title: string; subtitle: string; cta: string; badge: string }> = {
|
||
hero: { title: 'Новый промо-баннер', subtitle: 'Описание специального предложения', cta: 'Подробнее', badge: 'Промо' },
|
||
features: { title: 'Наши преимущества', subtitle: 'Ключевые особенности нашего сервиса', cta: '', badge: 'Сервис' },
|
||
pricing: { title: 'Тарифы и меню', subtitle: 'Выберите подходящую опцию', cta: 'Заказать', badge: 'Цены' },
|
||
testimonials: { title: 'Отзывы гостей', subtitle: 'Что говорят наши посетители', cta: '', badge: 'Отзывы' },
|
||
form: { title: 'Форма обратной связи', subtitle: 'Оставьте заявку и мы свяжемся с вами', cta: 'Отправить заявку', badge: 'Заявка' },
|
||
cta: { title: 'Присоединяйтесь к нам!', subtitle: 'Специальный бонус при первом визите', cta: 'Получить бонус', badge: 'Бонус' },
|
||
};
|
||
|
||
const info = typeTitles[type];
|
||
const newWidget: CmsWidget = {
|
||
id: `w-${type}-${ulid()}`,
|
||
type,
|
||
title: info.title,
|
||
subtitle: info.subtitle,
|
||
ctaText: info.cta || undefined,
|
||
badge: info.badge,
|
||
};
|
||
|
||
setWidgets((prev) => [...prev, newWidget]);
|
||
setSelectedWidgetId(newWidget.id);
|
||
showToast({
|
||
title: 'Виджет добавлен',
|
||
message: `Блок «${info.title}» добавлен на холст`,
|
||
type: 'success',
|
||
});
|
||
};
|
||
|
||
const handleMoveWidget = (index: number, direction: 'up' | 'down') => {
|
||
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
||
if (newIndex < 0 || newIndex >= widgets.length) return;
|
||
|
||
const newWidgets = [...widgets];
|
||
const temp = newWidgets[index];
|
||
newWidgets[index] = newWidgets[newIndex];
|
||
newWidgets[newIndex] = temp;
|
||
setWidgets(newWidgets);
|
||
};
|
||
|
||
const handleDeleteWidget = (id: string) => {
|
||
if (widgets.length <= 1) {
|
||
showToast({
|
||
title: 'Невозможно удалить',
|
||
message: 'На странице должен оставаться минимум один блок',
|
||
type: 'warning',
|
||
});
|
||
return;
|
||
}
|
||
const filtered = widgets.filter((w) => w.id !== id);
|
||
setWidgets(filtered);
|
||
if (selectedWidgetId === id) {
|
||
setSelectedWidgetId(filtered[0].id);
|
||
}
|
||
showToast({
|
||
title: 'Блок удален',
|
||
message: 'Виджет удален со страницы',
|
||
type: 'info',
|
||
});
|
||
};
|
||
|
||
const handleUpdateWidgetProperty = (field: keyof CmsWidget, val: string) => {
|
||
setWidgets((prev) =>
|
||
prev.map((w) => (w.id === selectedWidgetId ? { ...w, [field]: val } : w))
|
||
);
|
||
};
|
||
|
||
const handleSaveJson = () => {
|
||
showToast({
|
||
title: 'Макет сохранен',
|
||
message: `JSON-конфигурация (${widgets.length} виджетов) сохранена в локальное хранилище`,
|
||
type: 'success',
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-4 animate-fade-in">
|
||
{/* Header */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-krona-surface p-3.5 rounded-xl border border-krona-border shadow-krona-soft">
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<h2 className="text-sm font-bold text-krona-primary">Сайты & Krona CMS</h2>
|
||
<MaturityBadge level="target" size="sm" />
|
||
<Badge variant="gold" size="sm">krona.site/sushi-nsk</Badge>
|
||
</div>
|
||
<p className="text-xs text-krona-secondary mt-0.5">
|
||
Визуальный блочный конструктор посадочных страниц, формы захвата лидов, медиатека и SEO
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-1.5 overflow-x-auto text-xs">
|
||
{[
|
||
{ id: 'pages', label: 'Страницы', route: '/cms/pages' },
|
||
{ id: 'builder', label: 'Конструктор' },
|
||
{ id: 'forms', label: 'Формы', route: '/cms/forms' },
|
||
{ id: 'media', label: 'Медиа', route: '/cms/media' },
|
||
{ id: 'seo', label: 'SEO', route: '/cms/seo' },
|
||
{ id: 'domains', label: 'Домены', route: '/cms/domains' },
|
||
].map((tab) => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => {
|
||
setActiveTab(tab.id as any);
|
||
if (tab.route) navigate(tab.route);
|
||
}}
|
||
className={`px-3 py-1.5 rounded-md font-medium transition-colors whitespace-nowrap ${
|
||
activeTab === tab.id
|
||
? 'bg-[#2F684C] text-white shadow-sm'
|
||
: 'bg-krona-workspace text-krona-secondary hover:text-krona-primary border border-krona-border'
|
||
}`}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 1. PAGES TAB */}
|
||
{activeTab === 'pages' && (
|
||
<div className="bg-krona-surface border border-krona-border rounded-xl shadow-krona-soft overflow-hidden">
|
||
<table className="w-full text-left text-xs border-collapse">
|
||
<thead className="bg-krona-workspace/80 text-krona-secondary border-b border-krona-border">
|
||
<tr>
|
||
<th className="p-3 font-semibold">Название страницы</th>
|
||
<th className="p-3 font-semibold">URL / Slug</th>
|
||
<th className="p-3 font-semibold">Просмотров сегодня</th>
|
||
<th className="p-3 font-semibold">Дата редакции</th>
|
||
<th className="p-3 font-semibold text-center">Статус</th>
|
||
<th className="p-3 font-semibold text-right">Действие</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-krona-border/50">
|
||
{pages.map((p) => (
|
||
<tr key={p.id} className="hover:bg-krona-workspace/50 transition-colors">
|
||
<td className="p-3 font-medium text-krona-primary flex items-center gap-2">
|
||
<Globe className="w-4 h-4 text-krona-secondary" />
|
||
<span>{p.title}</span>
|
||
</td>
|
||
<td className="p-3 font-mono text-krona-secondary">{p.slug}</td>
|
||
<td className="p-3 font-bold text-krona-primary">{p.viewsToday}</td>
|
||
<td className="p-3 text-krona-secondary">{p.lastEdited}</td>
|
||
<td className="p-3 text-center">
|
||
<Badge variant="green" size="sm">Опубликована</Badge>
|
||
</td>
|
||
<td className="p-3 text-right">
|
||
<button
|
||
onClick={() => setActiveTab('builder')}
|
||
className="text-krona-green font-medium hover:underline inline-flex items-center gap-1"
|
||
>
|
||
<Edit3 className="w-3.5 h-3.5" />
|
||
Редактор
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
|
||
{/* 2. VISUAL BUILDER TAB */}
|
||
{activeTab === 'builder' && (
|
||
<div className="space-y-3">
|
||
{/* Builder Top Toolbar */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-krona-surface p-3 rounded-xl border border-krona-border shadow-krona-soft">
|
||
<div className="flex items-center gap-2">
|
||
<span className="font-bold text-xs text-krona-primary">
|
||
Редактирование: «Главная страница доставки»
|
||
</span>
|
||
<Badge variant="green" size="sm">Auto-save</Badge>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
{/* Responsive Viewport Switcher */}
|
||
<div className="flex items-center bg-krona-workspace p-1 rounded-lg border border-krona-border text-xs">
|
||
<button
|
||
onClick={() => setViewport('desktop')}
|
||
className={`p-1.5 rounded transition-colors ${
|
||
viewport === 'desktop' ? 'bg-[#2F684C] text-white shadow-sm' : 'text-krona-secondary'
|
||
}`}
|
||
title="Desktop (100%)"
|
||
>
|
||
<Monitor className="w-3.5 h-3.5" />
|
||
</button>
|
||
<button
|
||
onClick={() => setViewport('tablet')}
|
||
className={`p-1.5 rounded transition-colors ${
|
||
viewport === 'tablet' ? 'bg-[#2F684C] text-white shadow-sm' : 'text-krona-secondary'
|
||
}`}
|
||
title="Tablet (768px)"
|
||
>
|
||
<Tablet className="w-3.5 h-3.5" />
|
||
</button>
|
||
<button
|
||
onClick={() => setViewport('mobile')}
|
||
className={`p-1.5 rounded transition-colors ${
|
||
viewport === 'mobile' ? 'bg-[#2F684C] text-white shadow-sm' : 'text-krona-secondary'
|
||
}`}
|
||
title="Mobile (375px)"
|
||
>
|
||
<Smartphone className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
onClick={handleSaveJson}
|
||
className="text-xs"
|
||
>
|
||
<Download className="w-3.5 h-3.5 mr-1" />
|
||
Сохранить JSON
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
size="sm"
|
||
onClick={() => {
|
||
showToast({
|
||
title: 'Страница опубликована',
|
||
message: 'Новая версия сайта успешно развернута на krona.site/sushi-nsk',
|
||
type: 'success',
|
||
});
|
||
}}
|
||
className="text-xs"
|
||
>
|
||
Опубликовать
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Builder Workspace 3 Columns */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-3.5">
|
||
{/* Left: Widget Palette */}
|
||
<div className="lg:col-span-3 bg-krona-surface border border-krona-border rounded-xl p-3 space-y-3 shadow-krona-soft">
|
||
<div className="flex items-center justify-between pb-2 border-b border-krona-border">
|
||
<span className="text-xs font-bold text-krona-primary flex items-center gap-1.5">
|
||
<Layers className="w-3.5 h-3.5 text-krona-gold" /> Палитра виджетов
|
||
</span>
|
||
</div>
|
||
<div className="space-y-2 text-xs">
|
||
{[
|
||
{ type: 'hero' as const, label: 'Hero баннер', desc: 'Главный заголовок с призывом' },
|
||
{ type: 'features' as const, label: 'Feature Grid', desc: 'Сетка преимуществ с иконками' },
|
||
{ type: 'pricing' as const, label: 'Цены & Меню', desc: 'Карточки сетов и комбо' },
|
||
{ type: 'testimonials' as const, label: 'Отзывы гостей', desc: 'Карусель цитат с рейтингом' },
|
||
{ type: 'form' as const, label: 'Лид-форма', desc: 'Форма бронирования стола' },
|
||
{ type: 'cta' as const, label: 'CTA блок', desc: 'Финальный призыв к заказу' },
|
||
].map((item) => (
|
||
<button
|
||
key={item.type}
|
||
onClick={() => handleAddWidget(item.type)}
|
||
className="w-full text-left p-2.5 rounded-lg border border-krona-border hover:border-krona-green/70 bg-krona-workspace hover:bg-krona-surface transition-all flex items-center justify-between group"
|
||
>
|
||
<div>
|
||
<div className="font-semibold text-krona-primary group-hover:text-krona-green transition-colors">
|
||
+ {item.label}
|
||
</div>
|
||
<div className="text-[10px] text-krona-secondary">{item.desc}</div>
|
||
</div>
|
||
<Plus className="w-3.5 h-3.5 text-krona-secondary group-hover:text-krona-green" />
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Center: Canvas Preview */}
|
||
<div className="lg:col-span-6 bg-krona-workspace/60 border border-krona-border rounded-xl p-4 flex flex-col items-center overflow-y-auto min-h-[560px] max-h-[750px]">
|
||
<div
|
||
className={`w-full transition-all duration-300 space-y-3 ${
|
||
viewport === 'mobile'
|
||
? 'max-w-[375px] border-x border-krona-border shadow-md bg-krona-surface p-3 rounded-2xl'
|
||
: viewport === 'tablet'
|
||
? 'max-w-[640px] border border-krona-border shadow-md bg-krona-surface p-4 rounded-xl'
|
||
: 'max-w-full'
|
||
}`}
|
||
>
|
||
{widgets.map((w, idx) => {
|
||
const isSelected = w.id === selectedWidgetId;
|
||
|
||
return (
|
||
<div
|
||
key={w.id}
|
||
onClick={() => setSelectedWidgetId(w.id)}
|
||
className={`relative p-4 rounded-xl border transition-all cursor-pointer select-none bg-krona-surface shadow-sm ${
|
||
isSelected
|
||
? 'border-krona-green ring-2 ring-krona-green/30'
|
||
: 'border-krona-border hover:border-krona-border-strong'
|
||
}`}
|
||
>
|
||
{/* Widget Controls bar */}
|
||
<div className="flex items-center justify-between pb-2 mb-2 border-b border-krona-border/60">
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="font-bold text-[10px] uppercase text-krona-secondary">
|
||
#{idx + 1} {w.type}
|
||
</span>
|
||
{w.badge && <Badge variant="gold" size="sm">{w.badge}</Badge>}
|
||
</div>
|
||
|
||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||
<button
|
||
onClick={() => handleMoveWidget(idx, 'up')}
|
||
disabled={idx === 0}
|
||
className="p-1 rounded hover:bg-krona-workspace text-krona-secondary disabled:opacity-30"
|
||
title="Поднять выше"
|
||
>
|
||
<ArrowUp className="w-3 h-3" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleMoveWidget(idx, 'down')}
|
||
disabled={idx === widgets.length - 1}
|
||
className="p-1 rounded hover:bg-krona-workspace text-krona-secondary disabled:opacity-30"
|
||
title="Опустить ниже"
|
||
>
|
||
<ArrowDown className="w-3 h-3" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteWidget(w.id)}
|
||
className="p-1 rounded hover:bg-rose-500/10 text-krona-secondary hover:text-rose-600"
|
||
title="Удалить блок"
|
||
>
|
||
<Trash2 className="w-3 h-3" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Visual Widget Preview Rendering */}
|
||
<div className="space-y-1.5">
|
||
<h4 className="font-bold text-xs text-krona-primary leading-snug">
|
||
{w.title}
|
||
</h4>
|
||
<p className="text-[11px] text-krona-secondary leading-relaxed">
|
||
{w.subtitle}
|
||
</p>
|
||
{w.ctaText && (
|
||
<div className="pt-2">
|
||
<span className="inline-flex items-center px-3 py-1 rounded bg-[#2F684C] text-white text-[10px] font-semibold">
|
||
{w.ctaText} →
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: Selected Widget Properties Editor */}
|
||
<div className="lg:col-span-3 bg-krona-surface border border-krona-border rounded-xl p-3.5 space-y-3.5 shadow-krona-soft text-xs">
|
||
<div className="flex items-center justify-between pb-2 border-b border-krona-border">
|
||
<span className="font-bold text-krona-primary flex items-center gap-1.5">
|
||
<Settings2 className="w-3.5 h-3.5 text-krona-green" /> Свойства виджета
|
||
</span>
|
||
<span className="text-[10px] uppercase font-mono text-krona-secondary">
|
||
{selectedWidget.type}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="space-y-2.5">
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-semibold text-krona-secondary block">Заголовок блока:</label>
|
||
<input
|
||
type="text"
|
||
value={selectedWidget.title}
|
||
onChange={(e) => handleUpdateWidgetProperty('title', e.target.value)}
|
||
className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-xs text-krona-primary focus:outline-none focus:border-krona-green"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-semibold text-krona-secondary block">Подзаголовок / Текст:</label>
|
||
<textarea
|
||
rows={3}
|
||
value={selectedWidget.subtitle}
|
||
onChange={(e) => handleUpdateWidgetProperty('subtitle', e.target.value)}
|
||
className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-xs text-krona-primary focus:outline-none focus:border-krona-green resize-none"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-semibold text-krona-secondary block">Текст кнопки действия (CTA):</label>
|
||
<input
|
||
type="text"
|
||
value={selectedWidget.ctaText || ''}
|
||
onChange={(e) => handleUpdateWidgetProperty('ctaText', e.target.value)}
|
||
placeholder="Например: Заказать онлайн"
|
||
className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-xs text-krona-primary focus:outline-none focus:border-krona-green"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-semibold text-krona-secondary block">Бейдж / Тег:</label>
|
||
<input
|
||
type="text"
|
||
value={selectedWidget.badge || ''}
|
||
onChange={(e) => handleUpdateWidgetProperty('badge', e.target.value)}
|
||
placeholder="Например: Акция недели"
|
||
className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-xs text-krona-primary focus:outline-none focus:border-krona-green"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 3. FORMS TAB */}
|
||
{activeTab === 'forms' && (
|
||
<div className="bg-krona-surface border border-krona-border rounded-xl p-4 shadow-krona-soft space-y-4">
|
||
<div className="flex items-center justify-between pb-2 border-b border-krona-border">
|
||
<div>
|
||
<h4 className="text-xs font-bold text-krona-primary">Лид-формы и виджеты захвата заявок</h4>
|
||
<p className="text-[11px] text-krona-secondary">Формы автоматически создают лиды и сделки в CRM</p>
|
||
</div>
|
||
<Button variant="primary" size="sm">
|
||
<Plus className="w-3.5 h-3.5 mr-1" />
|
||
Новая форма
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3.5 text-xs">
|
||
<div className="p-3.5 rounded-xl border border-krona-border bg-krona-workspace space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-bold text-krona-primary text-sm">Форма быстрой брони столика</span>
|
||
<Badge variant="green" size="sm">Активна</Badge>
|
||
</div>
|
||
<p className="text-krona-secondary text-[11px]">Поля: Имя, Телефон, Дата/Время, Персоны, Пожелания</p>
|
||
<div className="flex justify-between pt-2 border-t border-krona-border/60 text-[11px]">
|
||
<span>Конверсия: 24.8%</span>
|
||
<span className="font-semibold text-krona-green">148 заявок</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-3.5 rounded-xl border border-krona-border bg-krona-workspace space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-bold text-krona-primary text-sm">Виджет подписки на скидку 20%</span>
|
||
<Badge variant="green" size="sm">Всплывающее окно</Badge>
|
||
</div>
|
||
<p className="text-krona-secondary text-[11px]">Поля: Номер телефона (с отправкой промокода в SMS/Telegram)</p>
|
||
<div className="flex justify-between pt-2 border-t border-krona-border/60 text-[11px]">
|
||
<span>Конверсия: 14.2%</span>
|
||
<span className="font-semibold text-krona-green">312 лидов</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 4. MEDIA LIBRARY TAB */}
|
||
{activeTab === 'media' && (
|
||
<div className="bg-krona-surface border border-krona-border rounded-xl p-4 shadow-krona-soft space-y-4">
|
||
<div className="flex items-center justify-between pb-2 border-b border-krona-border">
|
||
<div>
|
||
<h4 className="text-xs font-bold text-krona-primary">Медиабиблиотека сайта</h4>
|
||
<p className="text-[11px] text-krona-secondary">Фотографии блюд, баннеры акций, логотипы и видеоматериалы</p>
|
||
</div>
|
||
<Button variant="primary" size="sm">
|
||
<Plus className="w-3.5 h-3.5 mr-1" />
|
||
Загрузить файл
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||
{[
|
||
{ name: 'philadelphia_banner.jpg', size: '1.2 MB', category: 'Баннеры' },
|
||
{ name: 'tom_yam_dish.png', size: '840 KB', category: 'Каталог' },
|
||
{ name: 'autumn_set_promo.jpg', size: '2.1 MB', category: 'Акции' },
|
||
{ name: 'krona_hall_interior.jpg', size: '3.4 MB', category: 'Интерьер' },
|
||
].map((media, idx) => (
|
||
<div key={idx} className="p-3 rounded-xl border border-krona-border bg-krona-workspace space-y-2 text-xs">
|
||
<div className="h-28 rounded-lg bg-krona-surface border border-krona-border flex items-center justify-center text-krona-secondary">
|
||
<Image className="w-8 h-8 opacity-40" />
|
||
</div>
|
||
<div className="font-semibold text-krona-primary truncate">{media.name}</div>
|
||
<div className="flex justify-between text-[10px] text-krona-secondary">
|
||
<span>{media.category}</span>
|
||
<span>{media.size}</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 5. SEO TAB */}
|
||
{activeTab === 'seo' && (
|
||
<div className="bg-krona-surface border border-krona-border rounded-xl p-4 shadow-krona-soft space-y-4">
|
||
<div className="pb-2 border-b border-krona-border">
|
||
<h4 className="text-xs font-bold text-krona-primary">Поисковая оптимизация (SEO)</h4>
|
||
<p className="text-[11px] text-krona-secondary">Мета-теги, OpenGraph и сниппет поисковой выдачи Яндекс и Google</p>
|
||
</div>
|
||
|
||
<div className="space-y-3 max-w-xl text-xs">
|
||
<div className="space-y-1">
|
||
<label className="font-semibold text-krona-primary">Meta Title (Заголовок в поисковике):</label>
|
||
<input
|
||
type="text"
|
||
value={seoTitle}
|
||
onChange={(e) => setSeoTitle(e.target.value)}
|
||
className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-xs text-krona-primary focus:outline-none focus:border-krona-green"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="font-semibold text-krona-primary">Meta Description (Описание):</label>
|
||
<textarea
|
||
rows={3}
|
||
value={seoDescription}
|
||
onChange={(e) => setSeoDescription(e.target.value)}
|
||
className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-xs text-krona-primary focus:outline-none focus:border-krona-green resize-none"
|
||
/>
|
||
</div>
|
||
|
||
<div className="p-3 bg-krona-workspace rounded-xl border border-krona-border space-y-1">
|
||
<span className="text-[10px] uppercase text-krona-secondary font-bold">Превью в Яндекс:</span>
|
||
<div className="text-blue-600 text-sm font-semibold hover:underline cursor-pointer">
|
||
{seoTitle}
|
||
</div>
|
||
<div className="text-emerald-700 text-xs">krona.site/sushi-nsk</div>
|
||
<p className="text-xs text-krona-secondary">{seoDescription}</p>
|
||
</div>
|
||
|
||
<Button
|
||
variant="primary"
|
||
size="sm"
|
||
onClick={() => {
|
||
showToast({
|
||
title: 'SEO мета-теги сохранены',
|
||
message: 'Настройки поисковой оптимизации применены к сайту',
|
||
type: 'success',
|
||
});
|
||
}}
|
||
>
|
||
Сохранить SEO-настройки
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 6. DOMAINS TAB */}
|
||
{activeTab === 'domains' && (
|
||
<div className="bg-krona-surface border border-krona-border rounded-xl p-4 shadow-krona-soft space-y-4">
|
||
<div className="pb-2 border-b border-krona-border">
|
||
<h4 className="text-xs font-bold text-krona-primary">Подключение доменов & SSL-сертификаты</h4>
|
||
<p className="text-[11px] text-krona-secondary">Привязка собственного домена второго уровня к CMS</p>
|
||
</div>
|
||
|
||
<div className="space-y-3 max-w-xl text-xs">
|
||
<div className="p-3.5 bg-krona-workspace rounded-xl border border-krona-border flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Globe className="w-5 h-5 text-krona-green" />
|
||
<div>
|
||
<span className="font-bold text-krona-primary block">sushi-nsk.ru</span>
|
||
<span className="text-[10px] text-krona-secondary">SSL активен • Let's Encrypt Wildcard</span>
|
||
</div>
|
||
</div>
|
||
<Badge variant="green" size="sm">Подключен</Badge>
|
||
</div>
|
||
|
||
<div className="p-3.5 bg-krona-workspace rounded-xl border border-krona-border flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Globe className="w-5 h-5 text-krona-secondary" />
|
||
<div>
|
||
<span className="font-bold text-krona-primary block">krona.site/sushi-nsk</span>
|
||
<span className="text-[10px] text-krona-secondary">Системный поддомен платформы</span>
|
||
</div>
|
||
</div>
|
||
<Badge variant="neutral" size="sm">Основной</Badge>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|