Files
links/frontend/linktree-frontend/src/app/components/CustomizationPanel.tsx

1266 lines
62 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import React, { useState, useEffect } from 'react'
import { TemplatesSelector } from './TemplatesSelector'
import { ExportDataModal } from './ExportDataModal'
import { ImportDataModal } from './ImportDataModal'
import { designTemplates, DesignTemplate } from '../constants/designTemplates'
import { useLocale } from '../contexts/LocaleContext'
interface DesignSettings {
id?: number
template_id?: string
theme_color: string
background_image?: string
background_image_url?: string
dashboard_layout: 'sidebar' | 'grid' | 'list' | 'cards' | 'compact' | 'masonry' | 'timeline' | 'magazine'
groups_default_expanded: boolean
show_group_icons: boolean
show_link_icons: boolean
dashboard_background_color: string
font_family: string
custom_css: string
group_text_color?: string
link_text_color?: string
header_text_color?: string
cover_overlay_enabled?: boolean
cover_overlay_color?: string
cover_overlay_opacity?: number
// Новые опции кастомизации
group_overlay_enabled?: boolean
group_overlay_color?: string
group_overlay_opacity?: number
show_groups_title?: boolean
group_description_text_color?: string
body_font_family?: string
heading_font_family?: string
// Новые поля для цветового оверлея кнопок ссылок
link_overlay_enabled?: boolean
link_overlay_color?: string
link_overlay_opacity?: number
}
interface UserProfile {
id: number
username: string
email: string
full_name: string
bio?: string
avatar_url?: string
}
interface LinkItem {
id: number
title: string
url: string
icon_url?: string
group: number
}
interface Group {
id: number
name: string
description?: string
icon_url?: string
background_image_url?: string
is_public?: boolean
is_favorite?: boolean
links: LinkItem[]
}
interface CustomizationPanelProps {
isOpen: boolean
onClose: () => void
onSettingsUpdate: (settings: DesignSettings) => void
user?: UserProfile | null
groups?: Group[]
onDataUpdate?: () => void
}
export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, groups = [], onDataUpdate }: CustomizationPanelProps) {
const { t } = useLocale()
const [settings, setSettings] = useState<DesignSettings>({
theme_color: '#ffffff',
dashboard_layout: 'list',
groups_default_expanded: true,
show_group_icons: true,
show_link_icons: true,
dashboard_background_color: '#f8f9fa',
font_family: 'sans-serif',
custom_css: '',
group_text_color: '#333333',
link_text_color: '#666666',
header_text_color: '#000000'
})
const [loading, setLoading] = useState(false)
const [activeTab, setActiveTab] = useState<'layout' | 'colors' | 'groups' | 'templates' | 'advanced' | 'data'>('templates')
const [backgroundImageFile, setBackgroundImageFile] = useState<File | null>(null)
// Состояния для модалов экспорта/импорта
const [showExportModal, setShowExportModal] = useState(false)
const [showImportModal, setShowImportModal] = useState(false)
useEffect(() => {
if (isOpen) {
loadSettings()
}
}, [isOpen])
const loadSettings = async () => {
try {
const token = localStorage.getItem('token')
const API = process.env.NEXT_PUBLIC_API_URL || 'https://links.shareon.kr'
const response = await fetch(`${API}/api/customization/settings/`, {
headers: {
'Authorization': `Bearer ${token}`
}
})
if (response.ok) {
const data = await response.json()
setSettings(data)
}
} catch (error) {
console.error('Error loading settings:', error)
}
}
const saveSettings = async () => {
setLoading(true)
try {
const token = localStorage.getItem('token')
const API = process.env.NEXT_PUBLIC_API_URL || 'https://links.shareon.kr'
// Если есть новый файл фоновой картинки, отправляем через FormData
if (backgroundImageFile) {
const formData = new FormData()
// Добавляем все настройки
formData.append('template_id', settings.template_id || '')
formData.append('theme_color', settings.theme_color)
formData.append('dashboard_layout', settings.dashboard_layout)
formData.append('groups_default_expanded', settings.groups_default_expanded.toString())
formData.append('show_group_icons', settings.show_group_icons.toString())
formData.append('show_link_icons', settings.show_link_icons.toString())
formData.append('dashboard_background_color', settings.dashboard_background_color)
formData.append('font_family', settings.font_family)
formData.append('custom_css', settings.custom_css)
formData.append('header_text_color', settings.header_text_color || '#000000')
formData.append('group_text_color', settings.group_text_color || '#333333')
formData.append('link_text_color', settings.link_text_color || '#666666')
formData.append('cover_overlay_enabled', (settings.cover_overlay_enabled || false).toString())
formData.append('cover_overlay_color', settings.cover_overlay_color || '#000000')
formData.append('cover_overlay_opacity', (settings.cover_overlay_opacity || 0.3).toString())
formData.append('group_overlay_enabled', (settings.group_overlay_enabled || false).toString())
formData.append('group_overlay_color', settings.group_overlay_color || '#000000')
formData.append('group_overlay_opacity', (settings.group_overlay_opacity || 0.3).toString())
formData.append('show_groups_title', (settings.show_groups_title !== false).toString())
formData.append('group_description_text_color', settings.group_description_text_color || '#666666')
formData.append('body_font_family', settings.body_font_family || 'sans-serif')
formData.append('heading_font_family', settings.heading_font_family || 'sans-serif')
formData.append('link_overlay_enabled', (settings.link_overlay_enabled || false).toString())
formData.append('link_overlay_color', settings.link_overlay_color || '#000000')
formData.append('link_overlay_opacity', (settings.link_overlay_opacity || 0.2).toString())
formData.append('background_image', backgroundImageFile)
const response = await fetch(`${API}/api/customization/settings/`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
})
if (response.ok) {
const updatedSettings = await response.json()
onSettingsUpdate(updatedSettings)
setBackgroundImageFile(null) // Сбрасываем выбранный файл
onClose()
} else {
const errorData = await response.json()
console.error('Server error:', errorData)
}
} else {
// Если файл не выбран, отправляем только JSON настройки (картинка остается прежней)
const editableSettings = {
template_id: settings.template_id,
theme_color: settings.theme_color,
dashboard_layout: settings.dashboard_layout,
groups_default_expanded: settings.groups_default_expanded,
show_group_icons: settings.show_group_icons,
show_link_icons: settings.show_link_icons,
dashboard_background_color: settings.dashboard_background_color,
font_family: settings.font_family,
custom_css: settings.custom_css,
header_text_color: settings.header_text_color || '#000000',
group_text_color: settings.group_text_color || '#333333',
link_text_color: settings.link_text_color || '#666666',
cover_overlay_enabled: settings.cover_overlay_enabled || false,
cover_overlay_color: settings.cover_overlay_color || '#000000',
cover_overlay_opacity: settings.cover_overlay_opacity || 0.3,
group_overlay_enabled: settings.group_overlay_enabled || false,
group_overlay_color: settings.group_overlay_color || '#000000',
group_overlay_opacity: settings.group_overlay_opacity || 0.3,
show_groups_title: settings.show_groups_title !== false,
group_description_text_color: settings.group_description_text_color || '#666666',
body_font_family: settings.body_font_family || 'sans-serif',
heading_font_family: settings.heading_font_family || 'sans-serif',
link_overlay_enabled: settings.link_overlay_enabled || false,
link_overlay_color: settings.link_overlay_color || '#000000',
link_overlay_opacity: settings.link_overlay_opacity || 0.2
}
const response = await fetch(`${API}/api/customization/settings/`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(editableSettings)
})
if (response.ok) {
const updatedSettings = await response.json()
onSettingsUpdate(updatedSettings)
onClose()
} else {
const errorData = await response.json()
console.error('Server error:', errorData)
}
}
} catch (error) {
console.error('Error saving settings:', error)
} finally {
setLoading(false)
}
}
const handleChange = (field: keyof DesignSettings, value: any) => {
setSettings(prev => ({
...prev,
[field]: value
}))
}
const handleTemplateSelect = (template: DesignTemplate) => {
setSettings(prev => ({
...prev,
...template.settings,
id: prev.id, // Сохраняем оригинальный ID
template_id: template.id // Добавляем ID шаблона для отслеживания
}))
}
// Определяем текущий шаблон
const getCurrentTemplateId = () => {
// Если есть сохраненный template_id
if ((settings as any).template_id) {
return (settings as any).template_id
}
// Или пытаемся определить по совпадению настроек
for (const template of designTemplates) {
if (
template.settings.theme_color === settings.theme_color &&
template.settings.background_color === settings.dashboard_background_color &&
template.settings.dashboard_layout === settings.dashboard_layout
) {
return template.id
}
}
return undefined
}
if (!isOpen) return null
return (
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog modal-lg">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">
<i className="bi bi-palette me-2"></i>
{t('customization.title')}
</h5>
<button
type="button"
className="btn-close"
onClick={onClose}
></button>
</div>
<div className="modal-body">
{/* Вкладки */}
<ul className="nav nav-tabs mb-3">
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'layout' ? 'active' : ''}`}
onClick={() => setActiveTab('layout')}
>
<i className="bi bi-layout-sidebar me-1"></i>
{t('customization.layout')}
</button>
</li>
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'colors' ? 'active' : ''}`}
onClick={() => setActiveTab('colors')}
>
<i className="bi bi-palette-fill me-1"></i>
{t('customization.colors')}
</button>
</li>
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'groups' ? 'active' : ''}`}
onClick={() => setActiveTab('groups')}
>
<i className="bi bi-collection me-1"></i>
{t('customization.groups')}
</button>
</li>
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'templates' ? 'active' : ''}`}
onClick={() => setActiveTab('templates')}
>
<i className="bi bi-palette me-1"></i>
{t('customization.templates')}
</button>
</li>
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'advanced' ? 'active' : ''}`}
onClick={() => setActiveTab('advanced')}
>
<i className="bi bi-gear me-1"></i>
{t('customization.advanced')}
</button>
</li>
<li className="nav-item">
<button
className={`nav-link ${activeTab === 'data' ? 'active' : ''}`}
onClick={() => setActiveTab('data')}
>
<i className="bi bi-database me-1"></i>
{t('customization.data')}
</button>
</li>
</ul>
{/* Содержимое вкладок */}
<div className="tab-content">
{/* Вкладка: Макет */}
{activeTab === 'layout' && (
<div className="tab-pane fade show active">
<div className="row">
<div className="col-12 mb-4">
<label className="form-label fs-5 mb-3">
<i className="bi bi-layout-text-window-reverse me-2"></i>
{t('customization.layout.style')}
</label>
<div className="row g-3">
{[
{
value: 'list',
labelKey: 'customization.layout.list',
icon: 'bi-list-ul',
descriptionKey: 'customization.layout.listDescription'
},
{
value: 'grid',
labelKey: 'customization.layout.grid',
icon: 'bi-grid-3x3',
descriptionKey: 'customization.layout.gridDescription'
},
{
value: 'cards',
labelKey: 'customization.layout.cards',
icon: 'bi-card-heading',
descriptionKey: 'customization.layout.cardsDescription'
},
{
value: 'compact',
labelKey: 'customization.layout.compact',
icon: 'bi-layout-text-sidebar',
descriptionKey: 'customization.layout.compactDescription'
},
{
value: 'sidebar',
labelKey: 'customization.layout.sidebar',
icon: 'bi-layout-sidebar',
descriptionKey: 'customization.layout.sidebarDescription'
},
{
value: 'masonry',
labelKey: 'customization.layout.masonry',
icon: 'bi-bricks',
descriptionKey: 'customization.layout.masonryDescription'
},
{
value: 'timeline',
labelKey: 'customization.layout.timeline',
icon: 'bi-clock-history',
descriptionKey: 'customization.layout.timelineDescription'
},
{
value: 'magazine',
labelKey: 'customization.layout.magazine',
icon: 'bi-newspaper',
descriptionKey: 'customization.layout.magazineDescription'
},
{
value: 'test-list',
labelKey: 'customization.layout.testList',
icon: 'bi-list-check',
descriptionKey: 'customization.layout.testListDescription'
}
].map((layout) => (
<div key={layout.value} className="col-md-6 col-lg-4">
<div
className={`card text-center h-100 layout-option ${settings.dashboard_layout === layout.value ? 'border-primary bg-primary bg-opacity-10 selected' : 'border-secondary'}`}
style={{ cursor: 'pointer', transition: 'all 0.2s ease' }}
onClick={() => handleChange('dashboard_layout', layout.value)}
>
<div className="card-body d-flex flex-column">
<i className={`${layout.icon} fs-1 mb-3 text-primary`}></i>
<h6 className="card-title mb-2">{t(layout.labelKey)}</h6>
<p className="card-text small text-muted flex-grow-1">{t(layout.descriptionKey)}</p>
{settings.dashboard_layout === layout.value && (
<div className="mt-2">
<span className="badge bg-primary">
<i className="bi bi-check-lg me-1"></i>
Выбрано
</span>
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
<div className="col-12 mb-3">
<div className="alert alert-info">
<i className="bi bi-info-circle me-2"></i>
<strong>{t('customization.layout.tip')}</strong> {t('customization.layout.tipText')}
Каждый стиль имеет свои преимущества в зависимости от количества ссылок и их типа.
</div>
</div>
</div>
</div>
)}
{/* Вкладка: Цвета */}
{activeTab === 'colors' && (
<div className="tab-pane fade show active">
<div className="row">
<div className="col-md-6 mb-3">
<label className="form-label">{t('customization.colors.theme')}</label>
<div className="input-group">
<input
type="color"
className="form-control form-control-color"
value={settings.theme_color}
onChange={(e) => handleChange('theme_color', e.target.value)}
/>
<input
type="text"
className="form-control"
value={settings.theme_color}
onChange={(e) => handleChange('theme_color', e.target.value)}
/>
</div>
</div>
<div className="col-md-6 mb-3">
<label className="form-label">{t('customization.colors.background')}</label>
<div className="input-group">
<input
type="color"
className="form-control form-control-color"
value={settings.dashboard_background_color}
onChange={(e) => handleChange('dashboard_background_color', e.target.value)}
/>
<input
type="text"
className="form-control"
value={settings.dashboard_background_color}
onChange={(e) => handleChange('dashboard_background_color', e.target.value)}
/>
</div>
</div>
<div className="col-12 mb-3">
<label className="form-label">{t('customization.colors.backgroundImage')}</label>
<div className="mb-2">
<input
type="file"
className="form-control"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) {
setBackgroundImageFile(file)
}
}}
/>
<div className="form-text">
{t('customization.colors.backgroundImageHelp')}
</div>
</div>
{settings.background_image_url && (
<div className="mb-2">
<label className="form-label small">{t('customization.colors.currentImage')}</label>
<div className="d-flex align-items-center gap-2">
<img
src={settings.background_image_url}
alt={t('customization.colors.currentBackgroundAlt')}
className="img-thumbnail"
style={{ maxWidth: '200px', maxHeight: '100px', objectFit: 'cover' }}
/>
<button
type="button"
className="btn btn-outline-danger btn-sm"
onClick={() => {
handleChange('background_image_url', '')
}}
title={t('customization.colors.removeBackground')}
>
<i className="bi bi-trash"></i> {t('customization.colors.removeBackground')}
</button>
</div>
</div>
)}
{backgroundImageFile && (
<div className="mb-2">
<label className="form-label small">{t('customization.colors.newImage')}</label>
<div className="text-success">
<i className="bi bi-file-earmark-image me-1"></i>
{backgroundImageFile.name}
</div>
</div>
)}
</div>
<div className="col-md-4 mb-3">
<label className="form-label">{t('customization.colors.header')}</label>
<div className="input-group">
<input
type="color"
className="form-control form-control-color"
value={settings.header_text_color || '#000000'}
onChange={(e) => handleChange('header_text_color', e.target.value)}
/>
<input
type="text"
className="form-control"
value={settings.header_text_color || '#000000'}
onChange={(e) => handleChange('header_text_color', e.target.value)}
/>
</div>
</div>
<div className="col-md-4 mb-3">
<label className="form-label">{t('customization.colors.group')}</label>
<div className="input-group">
<input
type="color"
className="form-control form-control-color"
value={settings.group_text_color || '#333333'}
onChange={(e) => handleChange('group_text_color', e.target.value)}
/>
<input
type="text"
className="form-control"
value={settings.group_text_color || '#333333'}
onChange={(e) => handleChange('group_text_color', e.target.value)}
/>
</div>
</div>
<div className="col-md-4 mb-3">
<label className="form-label">{t('customization.colors.link')}</label>
<div className="input-group">
<input
type="color"
className="form-control form-control-color"
value={settings.link_text_color || '#666666'}
onChange={(e) => handleChange('link_text_color', e.target.value)}
/>
<input
type="text"
className="form-control"
value={settings.link_text_color || '#666666'}
onChange={(e) => handleChange('link_text_color', e.target.value)}
/>
</div>
</div>
</div>
</div>
)}
{/* Вкладка: Группы */}
{activeTab === 'groups' && (
<div className="tab-pane fade show active">
<div className="row">
<div className="col-12 mb-3">
<h6 className="text-muted">{t('customization.groups.displaySettings')}</h6>
</div>
<div className="col-12 mb-3">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
checked={settings.groups_default_expanded}
onChange={(e) => handleChange('groups_default_expanded', e.target.checked)}
/>
<label className="form-check-label">
Развернуть группы по умолчанию
</label>
</div>
</div>
<div className="col-12 mb-3">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
checked={settings.show_group_icons}
onChange={(e) => handleChange('show_group_icons', e.target.checked)}
/>
<label className="form-check-label">
Показывать иконки групп
</label>
</div>
</div>
<div className="col-12 mb-3">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
checked={settings.show_link_icons}
onChange={(e) => handleChange('show_link_icons', e.target.checked)}
/>
<label className="form-check-label">
Показывать иконки ссылок
</label>
</div>
</div>
{/* Новые настройки */}
<div className="col-12 mb-3">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
checked={settings.show_groups_title !== false}
onChange={(e) => handleChange('show_groups_title', e.target.checked)}
/>
<label className="form-check-label">
{t('customization.colors.showGroupsTitle')}
</label>
</div>
</div>
<div className="col-md-6 mb-3">
<label className="form-label">{t('customization.colors.groupDescription')}</label>
<div className="input-group">
<input
type="color"
className="form-control form-control-color"
value={settings.group_description_text_color || '#666666'}
onChange={(e) => handleChange('group_description_text_color', e.target.value)}
/>
<input
type="text"
className="form-control"
value={settings.group_description_text_color || '#666666'}
onChange={(e) => handleChange('group_description_text_color', e.target.value)}
/>
</div>
</div>
{/* Перекрытие групп цветом */}
<div className="col-12 mb-3">
<div className="card">
<div className="card-header">
<h6 className="mb-0">{t('customization.colors.groupOverlay')}</h6>
</div>
<div className="card-body">
<div className="form-check mb-3">
<input
className="form-check-input"
type="checkbox"
id="groupOverlayEnabled"
checked={settings.group_overlay_enabled || false}
onChange={(e) => handleChange('group_overlay_enabled', e.target.checked)}
/>
<label className="form-check-label" htmlFor="groupOverlayEnabled">
Включить цветовое перекрытие групп
</label>
</div>
{settings.group_overlay_enabled && (
<>
<div className="row">
<div className="col-6 mb-3">
<label className="form-label">Цвет перекрытия</label>
<div className="d-flex gap-2">
<input
type="color"
className="form-control form-control-color"
value={settings.group_overlay_color || '#000000'}
onChange={(e) => handleChange('group_overlay_color', e.target.value)}
title={t('customization.colors.chooseOverlayColor')}
/>
<input
type="text"
className="form-control"
value={settings.group_overlay_color || '#000000'}
onChange={(e) => handleChange('group_overlay_color', e.target.value)}
placeholder="#000000"
title={t('customization.colors.hexColorCode')}
/>
</div>
</div>
<div className="col-6 mb-3">
<label className="form-label">
Прозрачность ({Math.round((settings.group_overlay_opacity || 0.3) * 100)}%)
</label>
<input
type="range"
className="form-range"
min="0"
max="1"
step="0.1"
value={settings.group_overlay_opacity || 0.3}
onChange={(e) => handleChange('group_overlay_opacity', parseFloat(e.target.value))}
title="Настройка прозрачности перекрытия"
/>
</div>
</div>
{/* Preview */}
<div className="mb-3">
<label className="form-label">Предварительный просмотр</label>
<div className="position-relative rounded" style={{ height: '80px', border: '1px solid #dee2e6', overflow: 'hidden' }}>
<div
className="w-100 h-100 d-flex align-items-center justify-content-center text-white fw-bold"
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
}}
>
Пример группы
</div>
<div
className="position-absolute top-0 start-0 w-100 h-100"
style={{
backgroundColor: settings.group_overlay_color || '#000000',
opacity: settings.group_overlay_opacity || 0.3
}}
></div>
</div>
</div>
</>
)}
</div>
</div>
</div>
<div className="col-12">
<div className="alert alert-info">
<i className="bi bi-info-circle me-2"></i>
<strong>{t('customization.advanced.individualGroupSettings')}</strong><br/>
Чтобы настроить конкретную группу (публичность, избранное, разворачивание), используйте кнопку редактирования рядом с названием группы в основном списке.
</div>
</div>
{/* Секция цветового оверлея кнопок ссылок */}
<div className="col-12 mt-4">
<div className="border rounded p-3">
<div className="d-flex align-items-center justify-content-between mb-3">
<h6 className="mb-0">{t('customization.colors.linkOverlay')}</h6>
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
id="linkOverlay"
checked={settings.link_overlay_enabled || false}
onChange={(e) => {
console.log('Link overlay enabled:', e.target.checked)
handleChange('link_overlay_enabled', e.target.checked)
}}
/>
<label className="form-check-label" htmlFor="linkOverlay">
Включить цветовое перекрытие кнопок ссылок
</label>
</div>
</div>
<div className="row">
{settings.link_overlay_enabled && (
<>
<div className="col-md-6 mb-3">
<label className="form-label">Цвет перекрытия</label>
<input
type="color"
className="form-control form-control-color"
value={settings.link_overlay_color || '#000000'}
onChange={(e) => {
console.log('Link overlay color:', e.target.value)
handleChange('link_overlay_color', e.target.value)
}}
/>
</div>
<div className="col-md-6 mb-3">
<label className="form-label">
Прозрачность ({Math.round((settings.link_overlay_opacity || 0.2) * 100)}%)
</label>
<input
type="range"
className="form-range"
min="0"
max="1"
step="0.1"
value={settings.link_overlay_opacity || 0.2}
onChange={(e) => {
const value = parseFloat(e.target.value)
console.log('Link overlay opacity:', value)
handleChange('link_overlay_opacity', value)
}}
/>
</div>
<div className="col-12">
<label className="form-label">Предварительный просмотр</label>
<div className="position-relative rounded" style={{ height: '60px', border: '1px solid #dee2e6', overflow: 'hidden' }}>
<div
className="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
style={{
backgroundColor: '#f8f9fa',
color: '#333',
fontSize: '14px',
fontWeight: '500'
}}
>
Кнопка ссылки
</div>
<div
className="position-absolute top-0 start-0 w-100 h-100"
style={{
backgroundColor: settings.link_overlay_color || '#000000',
opacity: settings.link_overlay_opacity || 0.2,
}}
></div>
</div>
</div>
</>
)}
</div>
</div>
</div>
</div>
</div>
)}
{/* Вкладка: Шаблоны */}
{activeTab === 'templates' && (
<div className="tab-pane fade show active">
<TemplatesSelector
onTemplateSelect={handleTemplateSelect}
currentTemplate={getCurrentTemplateId()}
/>
</div>
)}
{/* Вкладка: Дополнительно */}
{activeTab === 'advanced' && (
<div className="tab-pane fade show active">
<div className="row">
<div className="col-12 mb-4">
<h6 className="text-muted">{t('customization.advanced.fontSettings')}</h6>
</div>
<div className="col-md-6 mb-3">
<label className="form-label">{t('customization.advanced.mainFont')}</label>
<select
className="form-select"
value={settings.font_family}
onChange={(e) => handleChange('font_family', e.target.value)}
>
<option value="sans-serif">{t('customization.advanced.systemSansSerif')}</option>
<option value="serif">{t('customization.advanced.systemSerif')}</option>
<option value="monospace">Monospace</option>
<option value="'PT Sans', sans-serif">PT Sans</option>
<option value="'PT Serif', serif">PT Serif</option>
<option value="'Roboto', sans-serif">Roboto</option>
<option value="'Open Sans', sans-serif">Open Sans</option>
<option value="'Source Sans Pro', sans-serif">Source Sans Pro</option>
<option value="'Fira Sans', sans-serif">Fira Sans</option>
<option value="'Ubuntu', sans-serif">Ubuntu</option>
<option value="'Yandex Sans Text', sans-serif">Yandex Sans Text</option>
<option value="'Inter', sans-serif">Inter</option>
<option value="'Manrope', sans-serif">Manrope</option>
<option value="'Nunito Sans', sans-serif">Nunito Sans</option>
</select>
</div>
<div className="col-md-6 mb-3">
<label className="form-label">{t('customization.advanced.headingFont')}</label>
<select
className="form-select"
value={settings.heading_font_family || settings.font_family}
onChange={(e) => handleChange('heading_font_family', e.target.value)}
>
<option value="">{t('customization.advanced.sameAsMain')}</option>
<option value="'PT Sans', sans-serif">PT Sans</option>
<option value="'PT Serif', serif">PT Serif</option>
<option value="'Roboto', sans-serif">Roboto</option>
<option value="'Open Sans', sans-serif">Open Sans</option>
<option value="'Source Sans Pro', sans-serif">Source Sans Pro</option>
<option value="'Fira Sans', sans-serif">Fira Sans</option>
<option value="'Ubuntu', sans-serif">Ubuntu</option>
<option value="'Yandex Sans Text', sans-serif">Yandex Sans Text</option>
<option value="'Inter', sans-serif">Inter</option>
<option value="'Manrope', sans-serif">Manrope</option>
<option value="'Montserrat', sans-serif">Montserrat</option>
<option value="'Playfair Display', serif">Playfair Display</option>
<option value="'Merriweather', serif">Merriweather</option>
<option value="'Oswald', sans-serif">Oswald</option>
<option value="'Russo One', sans-serif">Russo One</option>
<option value="'Comfortaa', cursive">Comfortaa</option>
<option value="'Philosopher', sans-serif">Philosopher</option>
<option value="'Cormorant Garamond', serif">Cormorant Garamond</option>
<option value="'Marck Script', cursive">Marck Script</option>
</select>
</div>
<div className="col-md-6 mb-3">
<label className="form-label">{t('customization.advanced.bodyFont')}</label>
<select
className="form-select"
value={settings.body_font_family || settings.font_family}
onChange={(e) => handleChange('body_font_family', e.target.value)}
>
<option value="">{t('customization.advanced.sameAsMain')}</option>
<option value="'PT Sans', sans-serif">PT Sans</option>
<option value="'PT Serif', serif">PT Serif</option>
<option value="'Roboto', sans-serif">Roboto</option>
<option value="'Open Sans', sans-serif">Open Sans</option>
<option value="'Source Sans Pro', sans-serif">Source Sans Pro</option>
<option value="'Fira Sans', sans-serif">Fira Sans</option>
<option value="'Ubuntu', sans-serif">Ubuntu</option>
<option value="'Yandex Sans Text', sans-serif">Yandex Sans Text</option>
<option value="'Inter', sans-serif">Inter</option>
<option value="'Manrope', sans-serif">Manrope</option>
<option value="'Nunito Sans', sans-serif">Nunito Sans</option>
<option value="'Lato', sans-serif">Lato</option>
<option value="'Source Serif Pro', serif">Source Serif Pro</option>
<option value="'Crimson Text', serif">Crimson Text</option>
<option value="Inter, sans-serif">Inter</option>
<option value="Roboto, sans-serif">Roboto</option>
<option value="Open Sans, sans-serif">Open Sans</option>
<option value="Source Sans Pro, sans-serif">Source Sans Pro</option>
<option value="Lato, sans-serif">Lato</option>
<option value="Nunito, sans-serif">Nunito</option>
<option value="Georgia, serif">Georgia</option>
<option value="Times New Roman, serif">Times New Roman</option>
</select>
</div>
<div className="col-12 mb-4">
<hr />
<h6 className="text-muted">{t('customization.advanced.additionalSettings')}</h6>
</div>
<div className="col-12 mb-3">
<label className="form-label">{t('customization.advanced.customCSS')}</label>
<textarea
className="form-control font-monospace"
rows={6}
value={settings.custom_css}
onChange={(e) => handleChange('custom_css', e.target.value)}
placeholder="/* Ваш дополнительный CSS */&#10;.my-custom-class {&#10; color: #333;&#10;}"
/>
</div>
{/* Cover Overlay Section */}
<div className="col-12 mb-3">
<div className="card">
<div className="card-header">
<h6 className="mb-0">Перекрытие обложки</h6>
</div>
<div className="card-body">
<div className="form-check mb-3">
<input
className="form-check-input"
type="checkbox"
id="coverOverlayEnabled"
checked={settings.cover_overlay_enabled || false}
onChange={(e) => handleChange('cover_overlay_enabled', e.target.checked)}
/>
<label className="form-check-label" htmlFor="coverOverlayEnabled">
Включить цветовое перекрытие обложки
</label>
</div>
{settings.cover_overlay_enabled && (
<>
<div className="row">
<div className="col-6 mb-3">
<label className="form-label">Цвет перекрытия</label>
<div className="d-flex gap-2">
<input
type="color"
className="form-control form-control-color"
value={settings.cover_overlay_color || '#000000'}
onChange={(e) => handleChange('cover_overlay_color', e.target.value)}
title="Выберите цвет перекрытия"
/>
<input
type="text"
className="form-control"
value={settings.cover_overlay_color || '#000000'}
onChange={(e) => handleChange('cover_overlay_color', e.target.value)}
placeholder="#000000"
title="Hex код цвета"
/>
</div>
</div>
<div className="col-6 mb-3">
<label className="form-label">
Прозрачность ({Math.round((settings.cover_overlay_opacity || 0.3) * 100)}%)
</label>
<input
type="range"
className="form-range"
min="0"
max="1"
step="0.1"
value={settings.cover_overlay_opacity || 0.3}
onChange={(e) => handleChange('cover_overlay_opacity', parseFloat(e.target.value))}
title="Настройка прозрачности перекрытия"
/>
</div>
</div>
{/* Preview */}
<div className="mb-3">
<label className="form-label">Предварительный просмотр</label>
<div className="position-relative" style={{ height: '100px', border: '1px solid #dee2e6', borderRadius: '0.375rem', overflow: 'hidden' }}>
<div
className="w-100 h-100"
style={{
background: 'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)',
backgroundSize: '20px 20px',
backgroundPosition: '0 0, 0 10px, 10px -10px, -10px 0px'
}}
></div>
<div
className="position-absolute top-0 start-0 w-100 h-100"
style={{
backgroundColor: settings.cover_overlay_color || '#000000',
opacity: settings.cover_overlay_opacity || 0.3
}}
></div>
</div>
</div>
</>
)}
</div>
</div>
</div>
</div>
</div>
)}
{/* Вкладка: Данные */}
{activeTab === 'data' && (
<div className="tab-pane fade show active">
<div className="row">
<div className="col-12 mb-4">
<h6 className="text-muted">
<i className="bi bi-database me-2"></i>
Экспорт и импорт данных профиля
</h6>
<p className="text-muted small">
Создавайте резервные копии данных профиля или восстанавливайте их из архива
</p>
</div>
{/* Экспорт данных */}
<div className="col-12 mb-4">
<div className="card">
<div className="card-header">
<h6 className="card-title mb-0">
<i className="bi bi-upload me-2"></i>
Экспорт данных
</h6>
</div>
<div className="card-body">
<p className="text-muted small mb-3">
Создать архив с данными профиля для резервного копирования или переноса
</p>
<button
type="button"
className="btn btn-outline-primary"
onClick={() => setShowExportModal(true)}
>
<i className="bi bi-download me-2"></i>
Создать экспорт
</button>
</div>
</div>
</div>
{/* Импорт данных */}
<div className="col-12 mb-4">
<div className="card">
<div className="card-header">
<h6 className="card-title mb-0">
<i className="bi bi-upload me-2"></i>
Импорт данных
</h6>
</div>
<div className="card-body">
<p className="text-muted small mb-3">
Загрузить и восстановить данные из архива экспорта
</p>
<div className="mb-3">
<label className="form-label">Выберите файл архива (.zip)</label>
<input
type="file"
className="form-control"
accept=".zip"
onChange={(e) => {
// TODO: Обработать загрузку файла и показать превью
const file = e.target.files?.[0]
if (file) {
console.log('Файл выбран:', file.name)
}
}}
/>
</div>
<button
type="button"
className="btn btn-outline-success"
onClick={() => setShowImportModal(true)}
>
<i className="bi bi-upload me-2"></i>
Открыть мастер импорта
</button>
</div>
</div>
</div>
{/* История операций */}
<div className="col-12">
<div className="card">
<div className="card-header">
<h6 className="card-title mb-0">
<i className="bi bi-clock-history me-2"></i>
История операций
</h6>
</div>
<div className="card-body">
<p className="text-muted">
Здесь будет отображаться история экспортов и импортов
</p>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
>
Отмена
</button>
<button
type="button"
className="btn btn-outline-warning"
onClick={() => {
if (confirm(t('customization.advanced.resetConfirm'))) {
// Сброс к дефолтным настройкам
const defaultSettings = {
theme_color: '#007bff',
background_image_url: '',
dashboard_layout: 'list' as const,
groups_default_expanded: true,
show_group_icons: true,
show_link_icons: true,
dashboard_background_color: '#ffffff',
font_family: 'Inter, sans-serif',
custom_css: '',
group_text_color: '',
link_text_color: '',
header_text_color: '',
cover_overlay_enabled: false,
cover_overlay_color: '#000000',
cover_overlay_opacity: 0.3,
group_overlay_enabled: false,
group_overlay_color: '#000000',
group_overlay_opacity: 0.3,
show_groups_title: true,
group_description_text_color: '',
body_font_family: 'Inter, sans-serif',
heading_font_family: 'Inter, sans-serif',
link_overlay_enabled: false,
link_overlay_color: '#000000',
link_overlay_opacity: 0.3
}
setSettings(defaultSettings)
onSettingsUpdate(defaultSettings)
}
}}
disabled={loading}
title={t('customization.resetSettings')}
>
<i className="bi bi-arrow-counterclockwise me-2"></i>
{t('customization.resetSettings')}
</button>
<button
type="button"
className="btn btn-primary"
onClick={saveSettings}
disabled={loading}
>
{loading ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
{t('common.saving')}
</>
) : (
<>
<i className="bi bi-check-lg me-2"></i>
{t('common.save')}
</>
)}
</button>
</div>
</div>
</div>
{/* Модалы экспорта и импорта */}
<ExportDataModal
isOpen={showExportModal}
onClose={() => setShowExportModal(false)}
user={user || null}
groups={groups}
/>
<ImportDataModal
isOpen={showImportModal}
onClose={() => setShowImportModal(false)}
onImportComplete={() => {
if (onDataUpdate) {
onDataUpdate()
}
}}
/>
</div>
)
}